Marketing API Specification¶
Overview¶
The Marketing API provides endpoints for the Marketing Planning module, managing team members, recipient lists, campaigns, activities, assignments, media, broadcasts, message templates, and change logs.
TQ-113: Audience segments have been replaced by Recipient Lists — first-class, reusable contact collections curated outside the activity and copied into broadcasts at compose time. The
marketing/audience/*andmarketing/activityaudience/*endpoint groups are deprecated.
Base Path: /marketing
Content Types:
- Request: application/json
- Response: application/json
Response Format¶
All endpoints return a TlinqApiResponse object:
Date Format: All dates are returned in ISO 8601 format (yyyy-MM-dd'T'HH:mm:ss)
Authentication¶
Team member identity is determined from the X-Email HTTP header from OAuth2-Proxy authentication.
Current User Endpoint¶
POST /marketing/currentuser¶
Gets the current authenticated user's team member information.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token |
Headers: | Header | Description | |--------|-------------| | X-Email | User email from OAuth2 | | X-Name | User name from OAuth2 |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"teamMemberId": 101,
"displayName": "John Smith",
"email": "john.smith@company.com",
"whatsappNum": "+971501234567",
"role": "Marketing Manager",
"active": true,
"createdAt": "2025-01-15T09:00:00"
}
}
Error Codes:
- MKT0100 - User email not in headers
- MKT0101 - User not registered as team member
- MKT0102 - Error checking user status
Team Member Endpoints¶
POST /marketing/teammember/list¶
Lists all active team members.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"teamMemberId": 101,
"displayName": "John Smith",
"email": "john.smith@company.com",
"whatsappNum": "+971501234567",
"role": "Marketing Manager",
"active": true
},
{
"teamMemberId": 102,
"displayName": "Jane Doe",
"email": "jane.doe@company.com",
"whatsappNum": "+971509876543",
"role": "Content Creator",
"active": true
},
{
"teamMemberId": 103,
"displayName": "Mike Johnson",
"email": "mike.johnson@company.com",
"whatsappNum": null,
"role": "Social Media Specialist",
"active": true
}
]
}
POST /marketing/teammember/create¶
Creates a new team member.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | displayName | string | Yes | Display name | | email | string | Yes | Email address | | whatsappNum | string | No | WhatsApp number | | role | string | No | Team role |
Request Example:
{
"session": "user-session-token",
"displayName": "Sarah Wilson",
"email": "sarah.wilson@company.com",
"whatsappNum": "+971505551234",
"role": "Graphic Designer"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"teamMemberId": 104,
"displayName": "Sarah Wilson",
"email": "sarah.wilson@company.com",
"whatsappNum": "+971505551234",
"role": "Graphic Designer",
"active": true,
"createdAt": "2025-06-15T10:30:00"
}
}
POST /marketing/teammember/read¶
Reads a team member by ID.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | teamMemberId | integer | Yes | Team member ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"teamMemberId": 101,
"displayName": "John Smith",
"email": "john.smith@company.com",
"whatsappNum": "+971501234567",
"role": "Marketing Manager",
"active": true,
"createdAt": "2025-01-15T09:00:00",
"updatedAt": "2025-06-10T14:30:00"
}
}
POST /marketing/teammember/write¶
Creates or updates a team member.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | teamMemberId | integer | No | Team member ID (omit for create) | | displayName | string | Yes | Display name | | email | string | Yes | Email address | | whatsappNum | string | No | WhatsApp number | | role | string | No | Team role | | active | boolean | No | Active status |
Request Example (Update):
{
"session": "user-session-token",
"teamMemberId": 101,
"displayName": "John M. Smith",
"role": "Senior Marketing Manager"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"teamMemberId": 101,
"displayName": "John M. Smith",
"email": "john.smith@company.com",
"role": "Senior Marketing Manager",
"active": true,
"updatedAt": "2025-06-15T11:00:00"
}
}
POST /marketing/teammember/delete¶
Soft-deletes a team member (sets inactive).
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | teamMemberId | integer | Yes | Team member ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"teamMemberId": 104,
"active": false,
"message": "Team member deactivated successfully"
}
}
Audience Segment Endpoints (DEPRECATED — TQ-113)¶
The marketing/audience/* and marketing/activityaudience/* endpoint groups are superseded by Recipient List Endpoints. The UI no longer calls them and the backing tables (nts.mkt_audience, nts.mkt_activityaudience) are scheduled for drop in a follow-up migration. Sections below are retained for reference only.
POST /marketing/audience/list¶
Lists all active audience segments.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"audienceId": 1,
"segmentName": "Luxury Travelers",
"description": "High-spending customers interested in premium experiences",
"active": true
},
{
"audienceId": 2,
"segmentName": "Family Vacationers",
"description": "Families with children looking for kid-friendly destinations",
"active": true
},
{
"audienceId": 3,
"segmentName": "Adventure Seekers",
"description": "Young adults interested in adventure and outdoor activities",
"active": true
}
]
}
POST /marketing/audience/create¶
Creates a new audience segment.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | segmentName | string | Yes | Segment name | | description | string | No | Description |
Request Example:
{
"session": "user-session-token",
"segmentName": "Business Travelers",
"description": "Corporate clients and business travelers requiring professional services"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"audienceId": 4,
"segmentName": "Business Travelers",
"description": "Corporate clients and business travelers requiring professional services",
"active": true,
"createdAt": "2025-06-15T10:30:00"
}
}
POST /marketing/audience/read¶
Reads an audience segment by ID.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"audienceId": 1,
"segmentName": "Luxury Travelers",
"description": "High-spending customers interested in premium experiences",
"active": true,
"createdAt": "2025-01-01T00:00:00",
"updatedAt": "2025-06-10T14:00:00"
}
}
POST /marketing/audience/write¶
Creates or updates an audience segment.
POST /marketing/audience/delete¶
Soft-deletes an audience segment.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"audienceId": 4,
"active": false,
"message": "Audience segment deactivated successfully"
}
}
Campaign Endpoints¶
POST /marketing/campaign/list¶
Lists all campaigns.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"campaignId": 1001,
"campaignName": "Summer 2025 Promotion",
"description": "Promote summer travel packages",
"goals": "Increase bookings by 25%",
"targetedApproach": "Multi-channel digital marketing",
"startDate": "2025-06-01",
"endDate": "2025-08-31",
"status": "ACTIVE"
},
{
"campaignId": 1002,
"campaignName": "Maldives Luxury Campaign",
"description": "Highlight luxury Maldives packages",
"goals": "Target high-value customers",
"targetedApproach": "Premium content and influencer partnerships",
"startDate": "2025-07-01",
"endDate": "2025-09-30",
"status": "DRAFT"
}
]
}
POST /marketing/campaign/create¶
Creates a new campaign.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignName | string | Yes | Campaign name | | description | string | No | Description | | goals | string | No | Campaign goals | | targetedApproach | string | No | Target approach |
Request Example:
{
"session": "user-session-token",
"campaignName": "Winter Holidays 2025",
"description": "Promote winter holiday destinations",
"goals": "Generate 500 qualified leads",
"targetedApproach": "Email marketing and social media ads"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"campaignId": 1003,
"campaignName": "Winter Holidays 2025",
"description": "Promote winter holiday destinations",
"goals": "Generate 500 qualified leads",
"targetedApproach": "Email marketing and social media ads",
"status": "DRAFT",
"createdAt": "2025-06-15T10:30:00"
}
}
POST /marketing/campaign/read¶
Reads a campaign by ID.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignId | integer | Yes | Campaign ID | | withDetails | boolean | No | Include activities and assignments |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"campaignId": 1001,
"campaignName": "Summer 2025 Promotion",
"description": "Promote summer travel packages",
"goals": "Increase bookings by 25%",
"targetedApproach": "Multi-channel digital marketing",
"startDate": "2025-06-01",
"endDate": "2025-08-31",
"status": "ACTIVE",
"createdAt": "2025-05-01T09:00:00",
"updatedAt": "2025-06-10T14:30:00",
"activities": [
{
"activityId": 2001,
"activityName": "Instagram Summer Series",
"activityType": "SOCIAL_MEDIA",
"status": "IN_PROGRESS",
"startDate": "2025-06-01",
"dueDate": "2025-06-30"
},
{
"activityId": 2002,
"activityName": "Email Newsletter - June",
"activityType": "EMAIL",
"status": "PENDING",
"startDate": "2025-06-15",
"dueDate": "2025-06-20"
}
]
}
}
POST /marketing/campaign/write¶
Creates or updates a campaign.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignId | integer | No | Campaign ID (omit for create) | | campaignName | string | Yes | Campaign name | | description | string | No | Description | | goals | string | No | Goals | | targetedApproach | string | No | Target approach | | startDate | string | No | Start date (yyyy-MM-dd) | | endDate | string | No | End date (yyyy-MM-dd) |
POST /marketing/campaign/changeStatus¶
Changes campaign status.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignId | integer | Yes | Campaign ID | | status | string | Yes | New status (DRAFT, ACTIVE, PAUSED, COMPLETED, CANCELLED) |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"campaignId": 1002,
"campaignName": "Maldives Luxury Campaign",
"status": "ACTIVE",
"statusChangedAt": "2025-06-15T11:00:00",
"message": "Campaign status changed successfully"
}
}
POST /marketing/campaign/delete¶
Soft-deletes a campaign (sets status to CANCELLED).
Activity Endpoints¶
POST /marketing/activity/list¶
Lists activities for a campaign.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignId | integer | Yes | Campaign ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"activityId": 2001,
"campaignId": 1001,
"activityName": "Instagram Summer Series",
"activityType": "SOCIAL_MEDIA",
"description": "Series of Instagram posts featuring summer destinations",
"storyText": null,
"startDate": "2025-06-01",
"dueDate": "2025-06-30",
"status": "IN_PROGRESS"
},
{
"activityId": 2002,
"campaignId": 1001,
"activityName": "Email Newsletter - June",
"activityType": "EMAIL",
"description": "Monthly newsletter featuring summer deals",
"storyText": "Dear Traveler, discover our exclusive summer offers...",
"startDate": "2025-06-15",
"dueDate": "2025-06-20",
"status": "PENDING"
}
]
}
POST /marketing/activity/create¶
Creates a new activity.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | campaignId | integer | Yes | Campaign ID | | activityName | string | Yes | Activity name | | activityType | string | Yes | Activity type (SOCIAL_MEDIA, EMAIL, BLOG, VIDEO, PRINT, EVENT) | | description | string | No | Description | | startDate | string | No | Start date (yyyy-MM-dd) | | dueDate | string | No | Due date (yyyy-MM-dd) |
Request Example:
{
"session": "user-session-token",
"campaignId": 1001,
"activityName": "Facebook Ads - Beach Resorts",
"activityType": "SOCIAL_MEDIA",
"description": "Targeted Facebook ads for beach resort packages",
"startDate": "2025-06-20",
"dueDate": "2025-07-15"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2003,
"campaignId": 1001,
"activityName": "Facebook Ads - Beach Resorts",
"activityType": "SOCIAL_MEDIA",
"description": "Targeted Facebook ads for beach resort packages",
"startDate": "2025-06-20",
"dueDate": "2025-07-15",
"status": "PENDING",
"createdAt": "2025-06-15T11:00:00"
}
}
POST /marketing/activity/read¶
Reads an activity by ID.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | withDetails | boolean | No | Include assignments and media |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2001,
"campaignId": 1001,
"activityName": "Instagram Summer Series",
"activityType": "SOCIAL_MEDIA",
"description": "Series of Instagram posts featuring summer destinations",
"storyText": "Escape to paradise this summer! 🌴",
"startDate": "2025-06-01",
"dueDate": "2025-06-30",
"status": "IN_PROGRESS",
"createdAt": "2025-05-20T10:00:00",
"updatedAt": "2025-06-10T15:30:00",
"assignments": [
{
"assignmentId": 3001,
"teamMemberId": 102,
"displayName": "Jane Doe",
"role": "Content Creator"
},
{
"assignmentId": 3002,
"teamMemberId": 103,
"displayName": "Mike Johnson",
"role": "Social Media Specialist"
}
],
"media": [
{
"mediaId": 4001,
"mediaType": "IMAGE",
"mediaUrl": "https://storage.example.com/summer-beach-1.jpg",
"filename": "summer-beach-1.jpg",
"description": "Beach sunset photo"
}
],
"audiences": [
{
"audienceId": 1,
"segmentName": "Luxury Travelers"
},
{
"audienceId": 3,
"segmentName": "Adventure Seekers"
}
]
}
}
POST /marketing/activity/updateStoryText¶
Updates activity story text.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | storyText | string | Yes | New story text |
Request Example:
{
"session": "user-session-token",
"activityId": 2001,
"storyText": "Escape to paradise this summer! 🌴 Book now and save up to 30% on selected beach resorts. Limited time offer!"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2001,
"storyText": "Escape to paradise this summer! 🌴 Book now and save up to 30% on selected beach resorts. Limited time offer!",
"updatedAt": "2025-06-15T11:30:00",
"message": "Story text updated successfully"
}
}
POST /marketing/activity/sendForReview¶
Sends activity for review.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | reviewerId | integer | Yes | Reviewer team member ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2001,
"status": "REVIEW",
"reviewerId": 101,
"reviewerName": "John Smith",
"sentForReviewAt": "2025-06-15T12:00:00",
"message": "Activity sent for review"
}
}
POST /marketing/activity/approve¶
Approves an activity.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2001,
"status": "APPROVED",
"approvedAt": "2025-06-15T14:00:00",
"approvedBy": 101,
"message": "Activity approved"
}
}
POST /marketing/activity/return¶
Returns an activity for revisions.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | reason | string | No | Return reason |
Request Example:
{
"session": "user-session-token",
"activityId": 2001,
"reason": "Please update the call-to-action text and add a discount code"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityId": 2001,
"status": "RETURNED",
"returnReason": "Please update the call-to-action text and add a discount code",
"returnedAt": "2025-06-15T14:30:00",
"returnedBy": 101,
"message": "Activity returned for revisions"
}
}
Assignment Endpoints¶
POST /marketing/assignment/assign¶
Assigns a team member to an activity.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | teamMemberId | integer | Yes | Team member ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"assignmentId": 3003,
"activityId": 2003,
"teamMemberId": 102,
"displayName": "Jane Doe",
"assignedAt": "2025-06-15T11:15:00"
}
}
POST /marketing/assignment/remove¶
Removes an assignment.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | assignmentId | integer | Yes | Assignment ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"message": "Assignment removed successfully"
}
}
POST /marketing/assignment/list¶
Lists assignments for an activity.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"assignmentId": 3001,
"activityId": 2001,
"teamMemberId": 102,
"displayName": "Jane Doe",
"role": "Content Creator",
"assignedAt": "2025-05-20T10:30:00"
},
{
"assignmentId": 3002,
"activityId": 2001,
"teamMemberId": 103,
"displayName": "Mike Johnson",
"role": "Social Media Specialist",
"assignedAt": "2025-05-20T10:35:00"
}
]
}
Media Endpoints¶
POST /marketing/media/add¶
Adds media to an activity.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | mediaType | string | Yes | Media type (IMAGE, VIDEO, DOCUMENT, AUDIO) | | mediaUrl | string | Yes | Media URL | | filename | string | No | Filename | | description | string | No | Description |
Request Example:
{
"session": "user-session-token",
"activityId": 2001,
"mediaType": "IMAGE",
"mediaUrl": "https://storage.example.com/maldives-sunset.jpg",
"filename": "maldives-sunset.jpg",
"description": "Sunset view from water villa"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"mediaId": 4002,
"activityId": 2001,
"mediaType": "IMAGE",
"mediaUrl": "https://storage.example.com/maldives-sunset.jpg",
"filename": "maldives-sunset.jpg",
"description": "Sunset view from water villa",
"createdAt": "2025-06-15T11:45:00"
}
}
POST /marketing/media/list¶
Lists media for an activity.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"mediaId": 4001,
"activityId": 2001,
"mediaType": "IMAGE",
"mediaUrl": "https://storage.example.com/summer-beach-1.jpg",
"filename": "summer-beach-1.jpg",
"description": "Beach sunset photo"
},
{
"mediaId": 4002,
"activityId": 2001,
"mediaType": "IMAGE",
"mediaUrl": "https://storage.example.com/maldives-sunset.jpg",
"filename": "maldives-sunset.jpg",
"description": "Sunset view from water villa"
}
]
}
POST /marketing/media/delete¶
Removes media.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | mediaId | integer | Yes | Media ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"message": "Media removed successfully"
}
}
Recipient List Endpoints¶
Recipient Lists (TQ-113) are system-wide, reusable contact collections that replace the per-activity audience/recipient model. A list is tied to a single channel (WHATSAPP or EMAIL) and holds members with optional CRM customer linkage. Lists populate broadcasts via list/copy-to-broadcast (append + dedupe + opt-out flag).
See Recipient Lists Feature for design and data model.
POST /marketing/list/list¶
Lists active recipient lists.
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| session | string | No | User session token |
| channel | string | No | Filter by WHATSAPP or EMAIL |
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"listId": 1,
"name": "VIP Customers",
"description": "High-value repeat guests",
"channel": "WHATSAPP",
"active": true,
"memberCount": 142,
"created": "2026-04-01T10:00:00",
"createdBy": 5,
"modified": "2026-04-10T09:20:00",
"modifiedBy": 5
}
]
}
Error Codes:
- MKT0500 — Generic read error
POST /marketing/list/read¶
Returns a single recipient list by ID.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | listId | integer | Yes | List ID |
Error Codes:
- MKT0501 — listId is required or list not found
POST /marketing/list/save¶
Creates or updates a recipient list. Pass a listId to update; omit to create.
Auth: agent, admin
Request Body (canonical CRecipientList fields):
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| listId | integer | On update | List ID |
| name | string | Yes | Display name |
| description | string | No | Sanitized as plain text |
| channel | string | Yes | WHATSAPP or EMAIL |
| active | boolean | No | Defaults to true on create |
Error Codes:
- MKT0502 — Validation failure (missing/invalid name, channel)
POST /marketing/list/delete¶
Soft-deletes a list (active = false). Members are preserved so the list can be reactivated in the database if needed.
Auth: admin
Error Codes:
- MKT0503 — listId is required or list not found
POST /marketing/list/members¶
Paginated read of list members.
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| listId | integer | Yes | List ID |
| offset | integer | No | Default 0 |
| limit | integer | No | Default 100; page size in the UI is 100 |
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"memberId": 8001,
"listId": 1,
"customerId": 301,
"recipientName": "Jane Doe",
"phone": "+971501234567",
"email": null,
"language": "en",
"created": "2026-04-10T09:21:07"
}
]
}
Error Codes:
- MKT0504 — listId is required
POST /marketing/list/member/add¶
Inserts a single member. Name is sanitized; phone is trimmed; email is lowercased and validated.
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| listId | integer | Yes | List ID |
| recipientName | string | No | Display name |
| phone | string | Conditional | Required for WhatsApp lists |
| email | string | Conditional | Required for Email lists |
| language | string | No | e.g. en, ar, ru, hi, sr |
Error Codes:
- MKT0505 — Missing required fields or list not found
POST /marketing/list/member/update¶
Partial update — only keys present in the request body are written. Same sanitization rules as member/add.
Auth: agent, admin
Error Codes:
- MKT0506 — memberId is required, member not found, or validation failure
POST /marketing/list/member/remove¶
Deletes a single member.
Auth: agent, admin
Error Codes:
- MKT0507 — memberId is required
POST /marketing/list/members/clear¶
Removes all members from a list.
Auth: agent, admin
Error Codes:
- MKT0508 — listId is required
POST /marketing/list/populate-from-crm¶
Clears existing members and refills from CRM. Phones are normalized to international format (+X… or 00X… → +X…); numbers without a country code are dropped. For Email lists, customers with doNotEmail set are excluded. Opt-outs are not applied — lists are snapshots.
An optional prefix filter further narrows results. The filter is applied against the normalized contact value (phone for WhatsApp lists, lowercased email for Email lists).
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| listId | integer | Yes | List ID |
| prefix | string | No | Prefix to filter on. Empty or absent = no filter. |
| prefixMode | string | No | include (default) — keep only contacts starting with prefix. exclude — drop contacts starting with prefix. |
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"memberCount": 122,
"totalCustomers": 1403,
"skippedNoContact": 890,
"skippedInvalidFormat": 261,
"skippedDoNotEmail": 0,
"skippedPrefixFilter": 130,
"skippedDuplicate": 0,
"truncated": false,
"maxMembers": 10000
}
}
Breakdown keys:
- memberCount — rows inserted
- totalCustomers — fetched from CRM
- skippedNoContact — customer has no phone (WA) or no email (Email)
- skippedInvalidFormat — WA only: phone exists but not in international format (no +/00)
- skippedDoNotEmail — Email only
- skippedPrefixFilter — dropped by the prefix/prefixMode filter
- skippedDuplicate — contact value already added
- truncated — capped by broadcast.max.recipients (default 10000)
Error Codes:
- MKT0509 — listId is required or list not found
POST /marketing/list/copy-to-broadcast¶
Append-plus-dedupe copy of a list's members into an activity's broadcast recipients. Opt-outs are checked per member and inserted with deliveryStatus = OPTED_OUT rather than skipped. The broadcast must be in DRAFT or READY.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | listId | integer | Yes | Source list | | broadcastId | integer | Yes | Target broadcast |
Response Structure:
added is the count of new recipients inserted (existing phones/emails are skipped). The broadcast's recipientCount is updated and DRAFT auto-promotes to READY when recipientCount > 0.
Error Codes:
- MKT0510 — Missing parameters, list not found, or broadcast not in DRAFT/READY
POST /marketing/list/export¶
Downloads the list as an Excel workbook. If the list is empty, returns a template with two channel-specific sample rows.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | listId | integer | Yes | List ID |
Response: application/octet-stream with Content-Disposition: attachment; filename="...". Filename is recipient_list_<listId>.xlsx for populated lists, recipient_list_template.xlsx for empty lists.
Frontend note: This endpoint returns binary. Use fetch() with getAuthHeaders(), not tlinq() — see recipient-list-edit.js exportToExcel().
Error Codes:
- MKT0511 — listId is required or list not found
POST /marketing/list/import¶
Upserts members from an Excel workbook. Match key is phone (WhatsApp lists) or email (Email lists). File size limit 5 MB, row limit 5000.
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| listId | integer | Yes | Target list |
| fileData | string | Yes | Base64-encoded .xlsx content |
Excel columns (header row required): Name, Phone, Email, Language.
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"inserted": 42,
"updated": 7,
"failed": 1,
"totalProcessed": 50,
"errors": ["Row 14: missing phone"]
}
}
Denormalized memberCount is refreshed inside the facade; no separate call is needed after import.
Error Codes:
- MKT0512 — Missing parameters, list not found, or file exceeds 5 MB
Activity Audience Endpoints (DEPRECATED — TQ-113)¶
Replaced by Recipient List endpoints. The UI no longer calls these; included for reference.
POST /marketing/activityaudience/add¶
Links an audience segment to an activity.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | audienceId | integer | Yes | Audience segment ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"activityAudienceId": 5001,
"activityId": 2001,
"audienceId": 2,
"segmentName": "Family Vacationers",
"addedAt": "2025-06-15T12:00:00"
}
}
POST /marketing/activityaudience/list¶
Lists audience segments for an activity.
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"activityAudienceId": 5001,
"activityId": 2001,
"audienceId": 1,
"segmentName": "Luxury Travelers"
},
{
"activityAudienceId": 5002,
"activityId": 2001,
"audienceId": 3,
"segmentName": "Adventure Seekers"
}
]
}
POST /marketing/activityaudience/delete¶
Removes audience from activity.
Change Log Endpoints¶
POST /marketing/changelog/list¶
Lists change logs for an entity.
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | entityType | string | Yes | Entity type (CAMPAIGN, ACTIVITY, TEAM_MEMBER) | | entityId | integer | Yes | Entity ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{
"changeLogId": 9001,
"entityType": "ACTIVITY",
"entityId": 2001,
"changeType": "STATUS_CHANGE",
"oldValue": "PENDING",
"newValue": "IN_PROGRESS",
"changedBy": 102,
"changedByName": "Jane Doe",
"changedAt": "2025-06-01T09:00:00"
},
{
"changeLogId": 9002,
"entityType": "ACTIVITY",
"entityId": 2001,
"changeType": "STORY_UPDATE",
"oldValue": null,
"newValue": "Escape to paradise this summer! 🌴",
"changedBy": 102,
"changedByName": "Jane Doe",
"changedAt": "2025-06-10T15:30:00"
},
{
"changeLogId": 9003,
"entityType": "ACTIVITY",
"entityId": 2001,
"changeType": "STATUS_CHANGE",
"oldValue": "IN_PROGRESS",
"newValue": "REVIEW",
"changedBy": 102,
"changedByName": "Jane Doe",
"changedAt": "2025-06-15T12:00:00"
}
]
}
Broadcast Endpoints¶
The Broadcast API manages multi-channel message delivery (WhatsApp, Email, SMS) for marketing activities. A broadcast is linked 1:1 with a marketing activity and goes through a lifecycle: configure template, preview audience, build recipients, then send.
POST /marketing/broadcast/get¶
Gets the broadcast configuration for an activity. Creates a default broadcast record if none exists.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"broadcastId": 6001,
"activityId": 2001,
"templateCsid": "HX1234567890abcdef",
"templateName": "summer_promo_2025",
"emailTemplateFile": "summer-deals.html",
"emailSubject": "Exclusive Summer Deals Just for You",
"messageBody": "Hello {{name}}, check out our summer deals!",
"selectedMediaId": 4001,
"status": "DRAFT",
"recipientCount": 0,
"sentCount": 0,
"deliveredCount": 0,
"failedCount": 0,
"createdAt": "2025-06-15T10:00:00",
"updatedAt": "2025-06-15T14:30:00"
}
}
Error Codes:
- MKT0200 - activityId is required
- MKT0201 - Activity not found
POST /marketing/broadcast/save¶
Updates the broadcast configuration for an activity.
Auth: agent, admin
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| session | string | No | User session token |
| activityId | integer | Yes | Activity ID |
| templateCsid | string | No | WhatsApp template content SID |
| templateName | string | No | WhatsApp template name |
| templateType | string | No | Template type: MEDIA (default, legacy media template) or CARD (whatsapp/card with media+buttons) |
| variableMapping | string | No | JSON mapping of variable positions to data sources (CARD type only). Example: {"1":"media_url","2":"recipient_name","3":"message_body"} |
| buttonConfig | string | No | JSON array of button URL suffixes (CARD type only). Example: [{"index":1,"urlSuffix":"summer2026"}] |
| emailTemplateFile | string | No | Email HTML template filename |
| emailSubject | string | No | Email subject line |
| messageBody | string | No | Message body text (supports {{placeholders}}) |
| selectedMediaId | integer | No | Media ID to attach to the broadcast |
Request Example:
{
"session": "user-session-token",
"activityId": 2001,
"templateCsid": "HX1234567890abcdef",
"templateName": "summer_promo_2025",
"emailSubject": "Exclusive Summer Deals Just for You",
"messageBody": "Hello {{name}}, check out our summer deals!",
"selectedMediaId": 4001
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"broadcastId": 6001,
"activityId": 2001,
"templateCsid": "HX1234567890abcdef",
"templateName": "summer_promo_2025",
"emailSubject": "Exclusive Summer Deals Just for You",
"messageBody": "Hello {{name}}, check out our summer deals!",
"selectedMediaId": 4001,
"status": "DRAFT",
"updatedAt": "2025-06-15T14:45:00"
}
}
Error Codes:
- MKT0200 - activityId is required
- MKT0201 - Activity not found
- MKT0202 - Broadcast not found for activity
POST /marketing/broadcast/preview-audience (REMOVED — TQ-113)¶
Removed. Recipient list member counts are tracked directly on CRecipientList.memberCount; the broadcast composer sums selected lists' counts client-side.
Legacy: POST /marketing/broadcast/preview-audience¶
Previews the CRM query results for an audience segment and channel. Returns a total count and a sample of matching contacts without persisting anything.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | audienceId | integer | Yes | Audience segment ID | | channel | string | Yes | Delivery channel (WHATSAPP, EMAIL, SMS) |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"audienceId": 1,
"channel": "WHATSAPP",
"totalCount": 1250,
"sample": [
{ "name": "Alice Johnson", "contactValue": "+971501234567" },
{ "name": "Bob Williams", "contactValue": "+971509876543" },
{ "name": "Carol Davis", "contactValue": "+971505551234" }
]
}
}
Error Codes:
- MKT0203 - audienceId is required
- MKT0204 - channel is required
- MKT0205 - Audience segment not found
POST /marketing/broadcast/build-recipients (REMOVED — TQ-113)¶
Removed. Use POST /marketing/list/copy-to-broadcast instead, which appends (rather than replaces) recipients from a curated list.
Legacy: POST /marketing/broadcast/build-recipients¶
Resolves the full recipient list from the CRM for the activity's linked audiences and persists them to the mkt_broadcast_recipient table. Replaces any previously built recipients.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID | | channel | string | Yes | Delivery channel (WHATSAPP, EMAIL, SMS) |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"broadcastId": 6001,
"activityId": 2001,
"channel": "WHATSAPP",
"recipientCount": 1247,
"status": "READY",
"builtAt": "2025-06-15T15:00:00",
"message": "Recipients built successfully"
}
}
Error Codes:
- MKT0200 - activityId is required
- MKT0204 - channel is required
- MKT0201 - Activity not found
- MKT0202 - Broadcast not found for activity
- MKT0206 - No audiences linked to activity
POST /marketing/broadcast/send¶
Triggers the asynchronous broadcast send. Requires the activity to be in APPROVED status and the broadcast to be in READY status (recipients already built). Sending runs in the background; poll the broadcast/get endpoint for progress.
Auth: admin only
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | activityId | integer | Yes | Activity ID |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"broadcastId": 6001,
"activityId": 2001,
"status": "SENDING",
"recipientCount": 1247,
"startedAt": "2025-06-15T16:00:00",
"message": "Broadcast send initiated"
}
}
Error Codes:
- MKT0200 - activityId is required
- MKT0201 - Activity not found
- MKT0202 - Broadcast not found for activity
- MKT0207 - Activity must be APPROVED before sending
- MKT0208 - Broadcast must be READY (recipients built) before sending
- MKT0209 - Broadcast is already sending or completed
POST /marketing/broadcast/recipients¶
Returns a paginated list of recipients for a broadcast.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | broadcastId | integer | Yes | Broadcast ID | | offset | integer | No | Pagination offset (default 0) | | limit | integer | No | Page size (default 50, max 200) |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"broadcastId": 6001,
"totalCount": 1247,
"offset": 0,
"limit": 50,
"recipients": [
{
"recipientId": 7001,
"contactName": "Alice Johnson",
"contactValue": "+971501234567",
"channel": "WHATSAPP",
"status": "DELIVERED",
"sentAt": "2025-06-15T16:01:00",
"deliveredAt": "2025-06-15T16:01:05",
"errorCode": null
},
{
"recipientId": 7002,
"contactName": "Bob Williams",
"contactValue": "+971509876543",
"channel": "WHATSAPP",
"status": "FAILED",
"sentAt": "2025-06-15T16:01:01",
"deliveredAt": null,
"errorCode": "21610"
}
]
}
}
Error Codes:
- MKT0202 - Broadcast not found
POST /marketing/broadcast/optout/add¶
Adds a contact to the opt-out register for a specific channel.
Auth: agent, admin
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | channel | string | Yes | Channel (WHATSAPP, EMAIL, SMS) | | contactValue | string | Yes | Phone number or email address | | reason | string | No | Opt-out reason |
Request Example:
{
"session": "user-session-token",
"channel": "WHATSAPP",
"contactValue": "+971501234567",
"reason": "Customer requested removal"
}
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"optoutId": 8001,
"channel": "WHATSAPP",
"contactValue": "+971501234567",
"reason": "Customer requested removal",
"createdAt": "2025-06-15T17:00:00",
"message": "Contact added to opt-out register"
}
}
Error Codes:
- MKT0204 - channel is required
- MKT0210 - contactValue is required
- MKT0211 - Contact already opted out for this channel
POST /marketing/broadcast/optout/remove¶
Removes a contact from the opt-out register.
Auth: admin only
Request Body: | Field | Type | Required | Description | |-------|------|----------|-------------| | session | string | No | User session token | | channel | string | Yes | Channel (WHATSAPP, EMAIL, SMS) | | contactValue | string | Yes | Phone number or email address |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"message": "Contact removed from opt-out register"
}
}
Error Codes:
- MKT0204 - channel is required
- MKT0210 - contactValue is required
- MKT0212 - Contact not found in opt-out register
POST /marketing/broadcast/webhook/twilio-status¶
Twilio delivery status callback endpoint. Called by Twilio when a message status changes (sent, delivered, failed, etc.). Accepts application/x-www-form-urlencoded form data.
Auth: unauthenticated (webhook)
Request Body (form-urlencoded): | Field | Type | Required | Description | |-------|------|----------|-------------| | MessageSid | string | Yes | Twilio message SID | | MessageStatus | string | Yes | Status (queued, sent, delivered, undelivered, failed) | | ErrorCode | string | No | Twilio error code (if failed) |
Response: HTTP 204 No Content
POST /marketing/broadcast/webhook/twilio-inbound¶
Twilio inbound message handler. Processes STOP/UNSUBSCRIBE messages and automatically adds the sender to the opt-out register. Accepts application/x-www-form-urlencoded form data.
Auth: unauthenticated (webhook)
Request Body (form-urlencoded): | Field | Type | Required | Description | |-------|------|----------|-------------| | From | string | Yes | Sender phone number (E.164 format) | | Body | string | Yes | Message body (checked for STOP/UNSUBSCRIBE keywords) |
Response: HTTP 204 No Content
GET /marketing/broadcast/optout/unsubscribe¶
Email one-click unsubscribe endpoint. Used in the List-Unsubscribe email header. Adds the email address to the opt-out register for the EMAIL channel.
Auth: unauthenticated (public link)
Query Parameters: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | email | string | Yes | Email address to unsubscribe | | channel | string | No | Channel (defaults to EMAIL) |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"message": "You have been unsubscribed successfully"
}
}
Message Template Endpoints¶
CRUD management for unified message templates (Twilio + Meta). Templates are referenced by broadcasts via templateId FK.
POST /marketing/template/list¶
List active templates, optionally filtered by channel and provider.
Auth: agent, admin
Request:
All filter fields are optional. Omit to list all active templates.
Response: Array of CMessageTemplate objects.
POST /marketing/template/read¶
Get a single template by ID.
Auth: agent, admin
Request:
Response: Single CMessageTemplate object.
POST /marketing/template/save¶
Create or update a template. Include templateId to update; omit to create.
Auth: admin only
Request:
{
"session": "",
"templateId": 1000,
"name": "Staycation Offer v2",
"channel": "WHATSAPP",
"provider": "META",
"metaTemplateName": "new_staycation_dxb_01",
"languageCode": "en",
"templateType": "MEDIA",
"variableMapping": "{\"1\":\"media_url\",\"2\":\"recipient_name\",\"3\":\"message_body\"}",
"componentSpec": "[{\"type\":\"header\",\"format\":\"image\",\"source\":\"media_url\"},{\"type\":\"body\",\"parameters\":[{\"source\":\"recipient_name\"},{\"source\":\"message_body\"}]}]",
"description": "April 2026 staycation campaign"
}
POST /marketing/template/delete¶
Soft-delete a template (sets active=false).
Auth: admin only
Request:
CMessageTemplate¶
| Field | Type | Description |
|---|---|---|
| templateId | integer | Template ID |
| name | string | Display name |
| channel | string | WHATSAPP or EMAIL |
| provider | string | TWILIO or META |
| twilioContentSid | string | Twilio Content SID (HXxxx) |
| metaTemplateName | string | Meta Business Manager template name |
| languageCode | string | Language code (en, en_US, ar, etc.) |
| templateType | string | MEDIA, CARD, or TEXT |
| variableMapping | string | JSON variable mapping |
| buttonConfig | string | JSON button configuration |
| componentSpec | string | JSON Meta component_spec |
| description | string | Admin notes |
| active | boolean | Active status |
Internal API Endpoints (Python WhatsApp Service)¶
These endpoints are used by the Python WhatsApp service for Meta broadcast dispatch, webhook processing, and AI CSR operations. They use INTERNAL_API_KEY Bearer token authentication (not session-based). All accept JSON POST.
Authentication: Authorization: Bearer <INTERNAL_API_KEY> — shared secret configured in whatsapp.python.service.api-key.
Role: internal — not accessible to guest, agent, or admin roles.
POST /marketing/internal/broadcast/dispatch-data¶
Returns everything Python needs to execute a broadcast in a single call.
Request:
Response:
{
"broadcast": { /* full CBroadcast fields */ },
"recipients": [ /* CBroadcastRecipient[] with deliveryStatus=PENDING */ ],
"optouts": [ /* CBroadcastOptout[] for this channel */ ],
"config": {
"waShortlink": "https://wa.me/yourshortlink",
"mediaCdnPrefix": "https://media.perunapps.com/",
"phonePrefixes": "0,00971,+971",
"phoneDefaultCc": "971"
}
}
POST /marketing/internal/recipient/status¶
Batch-update recipient delivery statuses. Accepts array of 1+ updates.
Request:
{
"updates": [
{
"recipientId": 5678,
"deliveryStatus": "SENT",
"messageSid": "wamid.HBgN...",
"errorCode": null,
"errorMessage": null,
"sentAt": "2026-04-05T14:30:00Z"
}
]
}
Response:
POST /marketing/internal/broadcast/stats¶
Recalculate broadcast aggregate stats from recipient records.
Request:
Response:
{ "recipientCount": 500, "sentCount": 480, "deliveredCount": 450, "failedCount": 20, "readCount": 200 }
POST /marketing/internal/broadcast/status¶
Update broadcast-level status (SENDING, SENT, FAILED).
Request:
Response:
POST /marketing/internal/recipient/lookup¶
Find recipient by messageSid (WAMID) for webhook status callbacks.
Request:
Response:
POST /marketing/internal/optout/add¶
Add opt-out entry for STOP keywords and Meta 131050 errors.
Request:
Response:
POST /marketing/internal/ai/config¶
Returns AI configuration so Python doesn't duplicate credentials.
Response:
{
"apiKey": "sk-ant-...",
"model": "claude-sonnet-4-5-20250929",
"maxTokens": 4096,
"timeoutSeconds": 30
}
POST /marketing/internal/ai/usage¶
Log AI usage for centralized cost tracking. Batch-capable.
Request:
{
"entries": [{
"provider": "anthropic",
"model": "claude-sonnet-4-5-20250929",
"inputTokens": 1250,
"outputTokens": 480,
"costUsd": 0.0109,
"context": "whatsapp_csr",
"contactPhone": "+971501234567"
}]
}
Response:
Data Models¶
CTeamMember¶
| Field | Type | Description |
|---|---|---|
| teamMemberId | integer | Team member ID |
| displayName | string | Display name |
| string | Email address | |
| whatsappNum | string | WhatsApp number |
| role | string | Team role |
| active | boolean | Active status |
| createdAt | datetime | Creation timestamp |
| updatedAt | datetime | Last update timestamp |
CAudienceSegment (DEPRECATED — TQ-113)¶
Replaced by CRecipientList. Retained for reference.
| Field | Type | Description |
|---|---|---|
| audienceId | integer | Segment ID |
| segmentName | string | Segment name |
| description | string | Description |
| active | boolean | Active status |
| createdAt | datetime | Creation timestamp |
| updatedAt | datetime | Last update timestamp |
CRecipientList¶
| Field | Type | Description |
|---|---|---|
| listId | integer | List ID |
| name | string | Display name |
| description | string | Optional notes |
| channel | string | WHATSAPP or EMAIL |
| active | boolean | Soft-delete flag |
| memberCount | integer | Denormalized member count (refreshed by facade) |
| created | datetime | Creation timestamp |
| createdBy | integer | Team member who created the list |
| modified | datetime | Last update timestamp |
| modifiedBy | integer | Team member who last modified the list |
CRecipientListMember¶
| Field | Type | Description |
|---|---|---|
| memberId | integer | Member ID |
| listId | integer | Parent list ID |
| customerId | integer | Optional CRM customer link (set when populated from CRM) |
| recipientName | string | Sanitized display name |
| phone | string | Trimmed phone; required for WhatsApp lists |
| string | Lowercased email; required for Email lists | |
| language | string | Optional language code (en, ar, ru, hi, sr) |
| created | datetime | Creation timestamp |
CCampaign¶
| Field | Type | Description |
|---|---|---|
| campaignId | integer | Campaign ID |
| campaignName | string | Campaign name |
| description | string | Description |
| goals | string | Campaign goals |
| targetedApproach | string | Target approach |
| startDate | date | Start date |
| endDate | date | End date |
| status | string | Status (DRAFT, ACTIVE, PAUSED, COMPLETED, CANCELLED) |
| createdAt | datetime | Creation timestamp |
| updatedAt | datetime | Last update timestamp |
| activities | array | List of activities (when withDetails=true) |
CActivity¶
| Field | Type | Description |
|---|---|---|
| activityId | integer | Activity ID |
| campaignId | integer | Parent campaign ID |
| activityName | string | Activity name |
| activityType | string | Type (SOCIAL_MEDIA, EMAIL, BLOG, VIDEO, PRINT, EVENT) |
| description | string | Description |
| storyText | string | Story/content text |
| startDate | date | Start date |
| dueDate | date | Due date |
| status | string | Status (PENDING, IN_PROGRESS, REVIEW, APPROVED, RETURNED, PUBLISHED, CANCELLED) |
| createdAt | datetime | Creation timestamp |
| updatedAt | datetime | Last update timestamp |
| assignments | array | List of assignments (when withDetails=true) |
| media | array | List of media (when withDetails=true) |
| audiences | array | List of target audiences (when withDetails=true) |
CActivityAssignment¶
| Field | Type | Description |
|---|---|---|
| assignmentId | integer | Assignment ID |
| activityId | integer | Activity ID |
| teamMemberId | integer | Team member ID |
| displayName | string | Team member display name |
| role | string | Team member role |
| assignedAt | datetime | Assignment timestamp |
CActivityMedia¶
| Field | Type | Description |
|---|---|---|
| mediaId | integer | Media ID |
| activityId | integer | Activity ID |
| mediaType | string | Type (IMAGE, VIDEO, DOCUMENT, AUDIO) |
| mediaUrl | string | Media URL |
| filename | string | Filename |
| description | string | Description |
| createdAt | datetime | Creation timestamp |
CBroadcast¶
| Field | Type | Description |
|---|---|---|
| broadcastId | integer | Broadcast ID |
| activityId | integer | Parent activity ID |
| templateCsid | string | WhatsApp template content SID |
| templateName | string | WhatsApp template name |
| emailTemplateFile | string | Email HTML template filename |
| emailSubject | string | Email subject line |
| messageBody | string | Message body text |
| selectedMediaId | integer | Attached media ID |
| status | string | Status (DRAFT, READY, SENDING, COMPLETED, FAILED) |
| recipientCount | integer | Total recipient count |
| sentCount | integer | Messages sent |
| deliveredCount | integer | Messages delivered |
| failedCount | integer | Messages failed |
| readCount | integer | Messages read |
| provider | string | Sending provider: TWILIO (default) or META |
| createdAt | datetime | Creation timestamp |
| updatedAt | datetime | Last update timestamp |
CBroadcastRecipient¶
| Field | Type | Description |
|---|---|---|
| recipientId | integer | Recipient ID |
| broadcastId | integer | Parent broadcast ID |
| contactName | string | Contact display name |
| contactValue | string | Phone number or email address |
| channel | string | Delivery channel (WHATSAPP, EMAIL, SMS) |
| status | string | Delivery status (PENDING, SENT, DELIVERED, FAILED) |
| sentAt | datetime | Sent timestamp |
| deliveredAt | datetime | Delivery confirmation timestamp |
| errorCode | string | Provider error code (if failed) |
CBroadcastOptout¶
| Field | Type | Description |
|---|---|---|
| optoutId | integer | Opt-out record ID |
| channel | string | Channel (WHATSAPP, EMAIL, SMS) |
| contactValue | string | Phone number or email address |
| reason | string | Opt-out reason |
| createdAt | datetime | Creation timestamp |
CChangeLog¶
| Field | Type | Description |
|---|---|---|
| changeLogId | integer | Change log ID |
| entityType | string | Entity type |
| entityId | integer | Entity ID |
| changeType | string | Type of change |
| oldValue | string | Previous value |
| newValue | string | New value |
| changedBy | integer | User who made change |
| changedByName | string | User display name |
| changedAt | datetime | Change timestamp |
Media File Upload¶
In addition to adding media by URL (POST /marketing/media/write), the Marketing module supports direct file uploads via the Media API. Uploaded files are stored on S3 under an auto-generated campaign prefix and served via CloudFront.
Endpoint: POST /media/marketing/upload (multipart/form-data)
This endpoint accepts image, PDF, audio, and video files with per-type size limits. The S3 key prefix is derived server-side from the campaign name and ID (e.g. campaigns/summer-dubai-2026-1042/). The client sends only the campaignId — no path or prefix input.
The returned CDN URL can then be saved as a media record via POST /marketing/media/write.
See Media API Specification for full endpoint documentation and Media Upload Feature for implementation details.