Messages
Messages represent dispatched communications to contacts across all channels.
List Messages
GET /api/workspaces/{workspaceId}/messages
Paginated list of messages with filtering.
Auth: Session required
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 50 | Items per page |
channel | string | — | EMAIL, PUSH, TELEGRAM, SMS, WHATSAPP, ONSITE, WEBHOOK |
status | string | — | QUEUED, SENDING, SENT, DELIVERED, OPENED, CLICKED, BOUNCED, FAILED, UNSUBSCRIBED |
contactId | string | — | Filter by contact |
campaignId | string | — | Filter by campaign |
Response:
{
"data": [
{
"id": "msg_abc123",
"channel": "EMAIL",
"status": "DELIVERED",
"subject": "Welcome to Acme!",
"sentAt": "2026-01-15T10:00:00.000Z",
"openedAt": "2026-01-15T10:05:00.000Z",
"contactId": "ct_1",
"campaignId": "cmp_1"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 5000,
"totalPages": 100
}
}Send Message
POST /api/workspaces/{workspaceId}/messages
Sends a message to a contact through the specified channel.
Auth: Session required
Request Body:
{
"contactId": "ct_abc123",
"channel": "EMAIL",
"subject": "Your weekly digest",
"body": "<h1>Hello {{contact.firstName}}</h1><p>Here is your update...</p>",
"templateId": "tpl_abc123",
"campaignId": "cmp_abc123",
"metadata": { "category": "digest" }
}| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | Yes | Target contact ID |
channel | string | Yes | EMAIL, PUSH, TELEGRAM, SMS, WHATSAPP, ONSITE, WEBHOOK |
subject | string | No | Message subject (email) |
body | string | No | Message body content |
templateId | string | No | Template to render |
campaignId | string | No | Associated campaign |
metadata | object | No | Additional metadata |
Response (200): Dispatch result. Returns 422 if dispatch fails.
Get Message
GET /api/workspaces/{workspaceId}/messages/{messageId}
Returns message details with contact, template, and campaign information.
Message Status Flow
QUEUED → SENDING → SENT → DELIVERED → OPENED → CLICKED
↘ BOUNCED
↘ FAILED
↘ UNSUBSCRIBEDEach status transition is tracked with timestamps and events.
Open tracking and image prefetch
Opens are counted when the tracking image in an email is loaded. Mail providers and security scanners load that image before a human sees the message, so CampaignOS ignores a load that both comes from a known proxy or scanner (Gmail’s image proxy, Yahoo, Outlook preview, Proofpoint, Mimecast and similar) and happens within 30 seconds of the send, and any load in the first 15 seconds whatever the agent.
Those loads are recorded on the message as machine opens and mark it delivered. The message detail shows how many there were. They do not set the open date, do not count in open rates, and do not trigger “opened the email” automations.
Gmail caches the image after fetching it, so a later human open may never reach the server. Clicks are the reliable engagement signal there, and a click also marks the message as opened.