Skip to content

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/* and marketing/activityaudience/* endpoint groups are deprecated.

Base Path: /marketing

Content Types: - Request: application/json - Response: application/json

Response Format

All endpoints return a TlinqApiResponse object:

{
  "apiStatus": {
    "errorCode": "OK",
    "errorMessage": "Success"
  },
  "apiData": { ... }
}

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:

{
  "session": "user-session-token"
}

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:

{
  "session": "user-session-token"
}

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:

{
  "session": "user-session-token",
  "teamMemberId": 101
}

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:

{
  "session": "user-session-token",
  "teamMemberId": 104
}

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:

{
  "session": "user-session-token"
}

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:

{
  "session": "user-session-token",
  "audienceId": 1
}

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:

{
  "session": "user-session-token",
  "audienceId": 4
}

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:

{
  "session": "user-session-token"
}

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:

{
  "session": "user-session-token",
  "campaignId": 1001,
  "withDetails": true
}

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:

{
  "session": "user-session-token",
  "campaignId": 1002,
  "status": "ACTIVE"
}

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:

{
  "session": "user-session-token",
  "campaignId": 1001
}

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:

{
  "session": "user-session-token",
  "activityId": 2001,
  "withDetails": true
}

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:

{
  "session": "user-session-token",
  "activityId": 2001,
  "reviewerId": 101
}

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:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "activityId": 2003,
  "teamMemberId": 102
}

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:

{
  "session": "user-session-token",
  "assignmentId": 3003
}

Response Structure:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": {
    "message": "Assignment removed successfully"
  }
}


POST /marketing/assignment/list

Lists assignments for an activity.

Request Example:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "mediaId": 4002
}

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: - MKT0501listId 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: - MKT0503listId 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: - MKT0504listId 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: - MKT0506memberId is required, member not found, or validation failure


POST /marketing/list/member/remove

Deletes a single member.

Auth: agent, admin

Error Codes: - MKT0507memberId is required


POST /marketing/list/members/clear

Removes all members from a list.

Auth: agent, admin

Error Codes: - MKT0508listId 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: - MKT0509listId 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:

{
  "apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
  "apiData": { "added": 87 }
}

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: - MKT0511listId 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:

{
  "session": "user-session-token",
  "activityId": 2001,
  "audienceId": 2
}

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:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "entityType": "ACTIVITY",
  "entityId": 2001
}

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:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "audienceId": 1,
  "channel": "WHATSAPP"
}

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:

{
  "session": "user-session-token",
  "activityId": 2001,
  "channel": "WHATSAPP"
}

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:

{
  "session": "user-session-token",
  "activityId": 2001
}

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:

{
  "session": "user-session-token",
  "broadcastId": 6001,
  "offset": 0,
  "limit": 50
}

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:

{
  "session": "user-session-token",
  "channel": "WHATSAPP",
  "contactValue": "+971501234567"
}

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:

GET /marketing/broadcast/optout/unsubscribe?email=alice@example.com&channel=EMAIL

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:

{ "session": "", "channel": "WHATSAPP", "provider": "META" }

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:

{ "session": "", "templateId": 1000 }

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:

{ "session": "", "templateId": 1000 }

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:

{ "broadcastId": 1234 }

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:

{ "updated": 1, "failed": 0 }

POST /marketing/internal/broadcast/stats

Recalculate broadcast aggregate stats from recipient records.

Request:

{ "broadcastId": 1234 }

Response:

{ "recipientCount": 500, "sentCount": 480, "deliveredCount": 450, "failedCount": 20, "readCount": 200 }

POST /marketing/internal/broadcast/status

Update broadcast-level status (SENDING, SENT, FAILED).

Request:

{ "broadcastId": 1234, "broadcastStatus": "SENT", "sendCompleted": "2026-04-05T15:00:00Z" }

Response:

{ "updated": true }

POST /marketing/internal/recipient/lookup

Find recipient by messageSid (WAMID) for webhook status callbacks.

Request:

{ "messageSid": "wamid.HBgN..." }

Response:

{ "recipient": { /* CBroadcastRecipient or null */ } }

POST /marketing/internal/optout/add

Add opt-out entry for STOP keywords and Meta 131050 errors.

Request:

{ "channel": "WHATSAPP", "contactValue": "+971501234567", "reason": "STOP keyword via WhatsApp" }

Response:

{ "optoutId": 99 }

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:

{ "logged": 1 }


Data Models

CTeamMember

Field Type Description
teamMemberId integer Team member ID
displayName string Display name
email 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
email 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.