Campaign API Documentation
Manage email marketing campaigns through programmatic API access. Create, update, monitor, and control campaign lifecycles.
Approve a Campaign
POST/api/v1/campaign.approveAPI Usage Notes
- Authentication is done by Admin API Key or admin SessionID
- Required privilege:
Reports(enforced whenADMIN_API_ENFORCE_PRIVILEGESis on) - v1 REST alias:
POST /api/v1/campaign.approve. Legacy access via/api.phpis also supported
Approves a campaign that is waiting for administrator approval (CampaignStatus Pending Approval) by setting its CampaignStatus to Ready, so the delivery engine can pick it up. This is the API equivalent of the approve action on the admin campaign screens. A campaign in any other status is refused with ErrorCode 5 and left unchanged. An administrator without user group limits may approve a campaign in any account. A sub-admin limited to some user groups may only approve campaigns of accounts in those groups, and gets ErrorCode 5003 for any other campaign and for a campaign that does not exist, so the answer does not reveal whether the campaign exists. CampaignID must be a plain positive integer, and any other form is answered with ErrorCode 2. Changed in v6.0.2.
Since v6.0.2 the approval records the version it approves (see What an Approval Covers below) and removes Options.ScreeningHold. Content screening runs again when the send starts, but a send whose campaign is still the approved version is not screened or held again. Any change made after the approval is screened anew. See Sender Screening.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.approve |
| SessionID | String | No | Admin session ID obtained from login |
| AdminAPIKey | String | No | Admin API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to approve |
| ExpectedFingerprint | String | No | The fingerprint of the campaign version the administrator reviewed. When given, the approval is refused with ErrorCode 4 if the campaign changed since, and nothing is changed. When omitted, the current version is approved. Added in v6.0.2. |
curl -X POST https://example.com/api/v1/campaign.approve \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.approve",
"AdminAPIKey": "your-admin-api-key",
"CampaignID": 12345
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"ErrorCode": [1]
}{
"Success": false,
"ErrorCode": 5,
"ErrorText": "Only a campaign that is pending approval can be approved. This campaign is Sent."
}0: Success
1: Missing required parameter (CampaignID)
2: Campaign not found, or CampaignID is not a plain positive integer (for example 1.234e3, 5.0 or a value with spaces)
3: The campaign could not be read to record the approval (retry)
4: The campaign changed after the administrator opened it for review (ExpectedFingerprint does not match, or the owner changed it while it was being approved), review it again
5: The campaign is not Pending Approval (also when it was approved by someone else, paused, cancelled or moved back to Draft while it was being approved), nothing was changed
5003: The campaign was not found or belongs to an account outside the user groups this sub-admin may accessWhat an Approval Covers
campaign.approve sets the campaign to Ready and records the version it approves: the campaign's emails (including A/B variations), sender, subject and other content, attachments, its audience, whether and how it repeats (Recursive and its recurrence settings) and its auto-resend settings. The Pending approval section of the staff dashboard sends ExpectedFingerprint with each approval, so a campaign its owner changed after the section was loaded is refused with ErrorCode 4 and stays in Pending Approval for another review. For an account with ReputationLevel Untrusted, a later change to any of these by the account moves the campaign back to Pending Approval, and a campaign whose content changed since the approval is not sent. The same approval releases a campaign that content screening held in Pending Approval, for any account, and the screening that runs when the send starts skips the approved version. Changed in v6.0.2.
Cancel a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Update - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.cancel |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to cancel |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.cancel",
"SessionID": "your-session-id",
"CampaignID": 12345
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"ErrorCode": [1]
}0: Success
1: Missing required parameter (CampaignID)
2: Campaign not found or doesn't belong to userStatus After Cancelling
A cancelled campaign is stored as Ready with ScheduleType Not Scheduled. A campaign in Pending Approval is withdrawn to Draft instead, because only an administrator releases a campaign from Pending Approval. If the internal status update is refused, campaign.cancel returns that failure instead of Success: true. Changed in v6.0.2.
Copy a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Create - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.copy |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to copy |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.copy",
"SessionID": "your-session-id",
"CampaignID": 12345
}'{
"Success": true,
"ErrorCode": 0,
"NewCampaignID": 12346
}{
"Success": false,
"ErrorCode": [1],
"ErrorText": ["Invalid source campaign"]
}0: Success
1: Invalid source campaign or campaign not foundCreate a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Create - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.create |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignName | String | Yes | Name of the new campaign |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.create",
"SessionID": "your-session-id",
"CampaignName": "Summer Sale 2025"
}'{
"Success": true,
"ErrorCode": 0,
"CampaignID": 12345
}{
"Success": false,
"ErrorCode": [1]
}0: Success
1: Missing required parameter (CampaignName)Get Campaign Details
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key or Admin API Key
- Required permissions:
Campaign.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to retrieve |
| RetrieveStatistics | Boolean | No | Include campaign statistics (default: false) |
| StatisticsDays | Integer | No | Time-series window in days for the per-day statistics blocks (Open/Click/etc.), measured from the campaign send start. Default: 15. Clamped to 1–365. |
| RetrieveTags | Boolean | No | Include campaign tags (default: false) |
| SplitABTestStatistics | Boolean | No | Include A/B test statistics (default: false) |
| RetrieveRecipientDomains | Boolean | No | Include recipient domain statistics (default: true) |
| FailOnNotFound | Boolean | No | Return an error when the campaign does not exist or is not owned by the authenticated user (default: false). When omitted or false, a missing campaign is not an error: the response is Success: true with an empty Campaign array. |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.get",
"SessionID": "your-session-id",
"CampaignID": 12345,
"RetrieveStatistics": true,
"RetrieveRecipientDomains": true
}'{
"Success": true,
"ErrorCode": 0,
"Campaign": {
"CampaignID": 12345,
"CampaignName": "Summer Sale 2025",
"CampaignStatus": "Sent",
"TotalRecipients": 10000,
"TotalSent": 9950,
"TotalOpens": 3500,
"UniqueOpens": 2100,
"TotalClicks": 850,
"UniqueClicks": 650
},
"CampaignThroughput": {
"EmailsPerSecond": 125.5,
"Duration": 79
}
}{
"Success": false,
"ErrorCode": 3
}0: Success
2: Invalid CampaignID (non-numeric)
3: Campaign not found or doesn't belong to user - only returned when FailOnNotFound=trueCampaign Not Found Is Not An Error By Default
Unless FailOnNotFound=true is passed, requesting a campaign that does not exist (or that belongs to another user) returns:
{
"Success": true,
"ErrorCode": 0,
"Campaign": []
}Callers that treat Success: true as "the campaign exists" must also check that Campaign is a non-empty object, or pass FailOnNotFound=true to get ErrorCode 3 instead.
ParentCampaign Is Absent When There Is No Parent
ParentCampaign is only added to the response when the campaign is an auto-resend and its parent campaign was found and is owned by the authenticated user. Otherwise the key is omitted entirely — it is not returned as null. Test with a key-existence check (isset() / array_key_exists() / 'ParentCampaign' in response.Campaign), not a null comparison.
Note that the sibling AutoResendCampaign key behaves differently: it is always present when applicable and returns an empty array ([]) when there is no auto-resend campaign, rather than being omitted.
:::
Pause a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Update - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.pause |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to pause |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.pause",
"SessionID": "your-session-id",
"CampaignID": 12345
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"ErrorCode": 3
}0: Success
1: Missing required parameter (CampaignID)
2: Campaign not found or doesn't belong to user
3: Invalid campaign status (campaign must be Sending or Ready to be paused)Get Campaign Recipients
GET/api/v1/campaign.recipients.getAPI Usage Notes
- Authentication is done by User API Key or Admin API Key
- Required permissions:
Campaign.Get - This endpoint only works for unsent campaigns (Draft, Ready, Pending Approval)
- For sent campaigns, use campaign statistics or queue endpoints instead
- v1 REST alias:
GET /api/v1/campaign.recipients.get. Legacy access via/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.recipients.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign |
| OnlyTotal | Boolean | No | Return only total count without recipient details (default: false) |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.recipients.get",
"SessionID": "your-session-id",
"CampaignID": 12345,
"OnlyTotal": false
}'{
"Success": true,
"ErrorCode": 0,
"CampaignID": 12345,
"TotalRecipients": 1500,
"Recipients": [
{
"SubscriberID": 101,
"EmailAddress": "user@example.com",
"FirstName": "John",
"LastName": "Doe"
}
]
}{
"Success": false,
"ErrorCode": 4,
"ErrorMessage": "This endpoint only works for unsent campaigns",
"CampaignStatus": "Sent"
}0: Success
1: Missing required parameter (CampaignID)
2: Invalid CampaignID (must be numeric)
3: Campaign not found or access denied
4: Invalid campaign status (must be Draft, Ready, or Pending Approval)
5: Campaign targeting definition unusable - no RulesJsonBundle, no criteria
defined, or a criterion carries an invalid list_id
6: A list referenced by the campaign's criteria is unavailable
10: Error retrieving campaign recipients (unexpected internal failure)Changed in v5.9.3
Codes 5 and 6 are returned with HTTP 200 and Success: false, like every other rejection from this endpoint.
Two conditions that used to be reported as the catch-all code 10 are now reported distinctly: an invalid list_id in a criterion is code 5, and a referenced list that is unavailable is code 6. Previously both arrived as code 10 with the message prefixed by Error retrieving campaign recipients:, which made them indistinguishable from a genuine server-side fault.
Code 6 deliberately does not distinguish "the list does not exist" from "the list belongs to another account" — the response is byte-identical in both cases, so it cannot be used to probe for the existence of other accounts' lists.
A database failure during the list lookup stays in the generic code 10 bucket. A server fault is never reported to you as "you referenced a list that does not exist".
Resume a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Update - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.resume |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to resume |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.resume",
"SessionID": "your-session-id",
"CampaignID": 12345
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"ErrorCode": 3
}0: Success
1: Missing required parameter (CampaignID)
2: Campaign not found or doesn't belong to user
3: Campaign status is not Paused (only Paused campaigns can be resumed)Untrusted Accounts
For an account with ReputationLevel Untrusted, a resumed campaign is stored as Pending Approval and waits for an administrator to approve it with campaign.approve. The response is still Success: true. Changed in v6.0.2.
If the internal status update is refused, campaign.resume now returns that failure instead of Success: true. Changed in v6.0.2.
Update a Campaign
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Update - Legacy endpoint access via
/api.phpis also supported
AI agent keys
With an AI agent key, an update that leaves the campaign scheduled or sending needs the send scope when it schedules the campaign or changes a scheduled or sending one beyond its name. A new campaign is unscheduled, so building it is a write. Such a send counts against the key's daily send budget and recipients cap (ErrorCode 100011), with the recipients counted as the current size of every included list and segment, and above the confirm threshold it first returns ErrorCode 100012 with a preview and a ConfirmToken to repeat the request with. Added in v6.0.2. See AI agent keys.
Content screening
Campaign.Update does not screen content, and its response is unchanged. When a Ready campaign starts sending (immediately, on its schedule, after a resume, as a recurring or auto-resend copy, or when an A/B test picks its winner), Octeth screens the emails and recipient lists of that send with fixed rules (spam score, a link whose text shows another domain than its target, credential forms, link shorteners, protected brand names, links reused from suspended accounts) and records the result for the operator. An edit made after scheduling is therefore screened too. By default this only flags. When the operator set the user group's content screening to hold, or a recipient list has a held import, the campaign moves to Pending Approval instead of sending, with a plain reason in Options.ScreeningHold.Reason, and waits for campaign.approve like any other held campaign. A version an administrator approved is not screened again. Links reused from suspended accounts only flag for the operator and never hold. Added in v6.0.2. See Sender Screening.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.update |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to update |
| CampaignName | String | No | New campaign name |
| CampaignStatus | String | No | Campaign status (Draft, Ready, Sending, Paused, Pending Approval, Sent, Failed) |
| CampaignStatusReason | String | No | Reason for status change |
| RelEmailID | Integer | No | ID of the email content to use |
| ScheduleType | String | No | Schedule type (Not Scheduled, Immediate, Future, Recursive) |
| SendDate | String | No | Send date (YYYY-MM-DD format) - required if ScheduleType=Future. Must be a valid future date (not 0000-00-00) |
| SendTime | String | No | Send time (HH:MM:SS format) - required if ScheduleType=Future. Combined with SendDate and SendTimeZone, the scheduled datetime must be in the future |
| SendTimeZone | String | No | Timezone for scheduled send. An HTML-encoded & is stored as &, so (US & Canada) is stored as (US & Canada). Changed in v6.0.2 |
| ScheduleRecDaysOfWeek | String | No | Days of week for recurring campaigns |
| ScheduleRecDaysOfMonth | String | No | Days of month for recurring campaigns |
| ScheduleRecMonths | String | No | Months for recurring campaigns - required if ScheduleType=Recursive |
| ScheduleRecHours | String | No | Hours for recurring campaigns - required if ScheduleType=Recursive |
| ScheduleRecMinutes | String | No | Minutes for recurring campaigns - required if ScheduleType=Recursive |
| ScheduleRecSendMaxInstance | Integer | No | Max instances for recurring campaigns - required if ScheduleType=Recursive |
| ApprovalUserExplanation | String | No | User explanation for approval |
| GoogleAnalyticsDomains | String | No | Google Analytics tracking domains |
| PublishOnRSS | String | No | Publish on RSS (Enabled/Disabled) |
| AutoResendEnabled | Boolean | No | Enable auto-resend to non-openers |
| AutoResendWaitDays | Integer | No | Days to wait before auto-resend - required if AutoResendEnabled=true |
| AutoResendSubject | String | No | Subject line for auto-resend - required if AutoResendEnabled=true |
| AutoResendPreHeaderText | String | No | Pre-header text for auto-resend. If omitted or empty, the auto-resend uses the original campaign's pre-header. |
| OriginalCampaignID | Integer | No | ID of original campaign if this is a resend |
| RecipientListsAndSegments | String | No | Comma-separated list (format: ListID:SegmentID) |
| Exclude_RecipientListsAndSegments | String | No | Comma-separated exclusion list (format: ListID:SegmentID) |
| RulesJsonBundle | String | No | JSON string with advanced recipient selection rules. Every list_id in criteria and every segments rule value must be an existing list or segment owned by the authenticated user, otherwise the update is refused with ErrorCode 28 (or 29 for a value that is not a positive integer) and nothing is changed. An empty list_id names no list and is ignored. |
| S2SEnabled | Boolean | No | Enable server-to-server tracking |
| ABTesting | Object | No | A/B testing configuration (see A/B Testing Parameters below) |
A/B Testing Parameters:
When the ABTesting object is provided, the campaign is configured as an A/B split test. Each variation references a separate email (created via Email.Create + Email.Update) and is assigned a weight that determines the distribution percentage across recipients.
| Parameter | Type | Required | Description |
|---|---|---|---|
| ABTesting[Variations] | Array | Yes | Array of variation objects (minimum 2, maximum 5) |
| ABTesting[Variations][N][emailid] | Integer | Yes | Email ID for this variation (must belong to the authenticated user) |
| ABTesting[Variations][N][weight] | Integer | Yes | Relative weight for distribution (must be > 0). Distribution percentages are calculated automatically from the weights. For example, two variations with weight 1 each results in 50%/50% distribution. Weights of 1, 1, 2 result in 25%/25%/50% |
Important Notes
- When A/B testing is enabled, the campaign's
RelEmailIDis automatically set to0— do not pass aRelEmailIDalongsideABTesting. - Each variation must have a distribution of at least 1%.
- To disable A/B testing on a campaign, pass an empty
ABTestingparameter. - The system randomly distributes recipients across variations during queue generation, so each subscriber receives exactly one variation.
Pending Approval
Only an administrator releases a campaign from Pending Approval, with campaign.approve. While a campaign is in Pending Approval, campaign.update accepts CampaignStatus of Pending Approval or Draft (to withdraw it and edit it) and refuses any other status with ErrorCode 30, leaving the campaign unchanged. Updates that do not send CampaignStatus are accepted as before. This applies to every account.
When the campaign's owner has a ReputationLevel of Untrusted, every update that arms the campaign to send is stored as Pending Approval and an approval notification is sent to the administrators. A campaign is armed when it is Sending, or Ready with a ScheduleType other than Not Scheduled. The hold applies when the campaign was not armed before the update: Draft, Ready with Not Scheduled (the state of a new campaign), Paused, Sent or Failed. An update between armed states, such as rescheduling an approved campaign, is not held again. Changed in v6.0.2: before, only a Draft to Ready update was held.
An approval covers one version of the campaign: its emails (including A/B variations and split test emails), sender, subject and other email content, attachments, audience (RulesJsonBundle, RecipientListsAndSegments, ExcludeRecipientListsAndSegments) and auto-resend settings (AutoResendEnabled, AutoResendSubject, AutoResendPreHeaderText). For an Untrusted owner, an update that changes any of these while the campaign is armed, Paused or Pending Approval stores it as Pending Approval again, whatever CampaignStatus the request named. The schedule is part of the approved version only as far as it decides how often the campaign sends: a new SendDate, SendTime or SendTimeZone, or a switch between Immediate, Future and Not Scheduled, keeps the approval of a one-time campaign. Changing ScheduleType to or from Recursive, or changing any recurrence setting of a Recursive campaign (ScheduleRecDaysOfWeek, ScheduleRecDaysOfMonth, ScheduleRecMonths, ScheduleRecHours, ScheduleRecMinutes, ScheduleRecSendMaxInstance), needs a new approval. When the campaign is about to send, a campaign of an Untrusted owner whose content no longer matches the approved version, or that has no approval on record, is moved back to Pending Approval instead of sending. Content screening runs at the same moment for every account (see Content screening above), and a campaign it holds also moves to Pending Approval. Changed in v6.0.2.
An update that omits ExcludeRecipientListsAndSegments clears the campaign's exclusion lists (existing behavior). For an Untrusted owner that is an audience change, so it re-holds an approved campaign that had exclusions.
Administrators receive at most one approval email per campaign per hour, so moving a campaign between Draft and Ready repeatedly does not send repeated emails. Campaigns held when they come up to send, by the approval check or by content screening, are reported in one summary email. Changed in v6.0.2.
The API still returns Success: true when the update is held, and the response does not indicate that the status was changed from the one requested. Re-read the campaign with campaign.get to confirm the resulting status.
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.update",
"SessionID": "your-session-id",
"CampaignID": 12345,
"CampaignName": "Updated Summer Sale",
"CampaignStatus": "Ready",
"ScheduleType": "Future",
"SendDate": "2025-06-15",
"SendTime": "10:00:00",
"SendTimeZone": "America/New_York"
}'{
"Success": true,
"ErrorCode": 0
}{
"Success": false,
"ErrorCode": 6
}0: Success
1: Missing required parameter (CampaignID)
2: Campaign not found or doesn't belong to user
3: Invalid campaign status
4: Email not found or doesn't belong to user
5: Invalid schedule type
6: Missing or invalid SendDate for Future schedule (empty, null, or 0000-00-00)
7: Missing SendTime for Future schedule
8: Missing ScheduleRecDaysOfWeek or ScheduleRecDaysOfMonth for Recursive schedule
9: Missing ScheduleRecMonths for Recursive schedule
10: Missing ScheduleRecHours for Recursive schedule
11: Missing ScheduleRecMinutes for Recursive schedule
12: Missing ScheduleRecSendMaxInstance for Recursive schedule
14: Missing or invalid AutoResendWaitDays when AutoResendEnabled=true
15: Missing AutoResendSubject when AutoResendEnabled=true
17: Invalid ABTesting parameter format (must be array)
18: Invalid ABTesting variations format (must be array)
19: Email variation not found or doesn't belong to user
21: Minimum 2 A/B test variations required
22: Maximum 5 A/B test variations allowed
23: Missing required fields in A/B variation (EmailID, Weight)
24: Invalid EmailID or Weight in A/B variation
25: A/B variation distribution percentage below 1%
26: Auto-resend cannot be used with repeating (recursive) campaigns
27: Scheduled send date and time is in the past
28: RulesJsonBundle names a list or segment that does not exist or does not belong to the user
29: RulesJsonBundle holds a list_id or segments rule value that is not a positive integer
30: The campaign is in Pending Approval and the requested CampaignStatus is not Draft or Pending Approval (only campaign.approve releases it)Get Campaign Archive URL
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaigns.Get - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.archive.geturl |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| TagID | Integer | Yes | ID of the tag for archive URL |
| TemplateURL | String | No | Custom template URL for archive |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaigns.archive.geturl",
"SessionID": "your-session-id",
"TagID": 5,
"TemplateURL": "https://example.com/archive-template"
}'{
"Success": true,
"ErrorCode": 0,
"URL": "https://example.com/archive/tag/5"
}{
"Success": false,
"ErrorCode": 2
}0: Success
1: Missing required parameter (TagID)
2: Tag not found or doesn't belong to userDelete Campaigns
POST/api.php (legacy)API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaign.Delete - This endpoint also deletes associated auto-resend campaigns automatically
- Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.delete |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| Campaigns | String | Yes | Comma-separated list of campaign IDs to delete |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaigns.delete",
"SessionID": "your-session-id",
"Campaigns": "12345,12346,12347"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": ""
}{
"Success": false,
"ErrorCode": [1]
}0: Success
1: Missing required parameter (Campaigns)Get Campaigns List
GET/api/v1/campaigns.getAPI Usage Notes
- Authentication required: User API Key. Admin authentication is also accepted with
Access=adminandUserID(see Admin usage below) - Required permissions:
Campaigns.Get - v1 REST alias:
GET /api/v1/campaigns.get. Legacy access via/api.phpis also supported
Admin usage (v5.9.6, #2775)
This command also accepts admin authentication. Pass AdminAPIKey (or an admin SessionID), Access=admin, and UserID naming the account to act on; the response is exactly what that account's own API key would receive. Without Access=admin the call is treated as a user call, so existing integrations are unaffected. UserID is ignored under user authentication. Admin-only error codes: 5001 UserID missing or invalid, 5002 user not found, 5003 user outside the user groups a restricted sub-admin may access. Requires the User.Edit privilege when ADMIN_API_ENFORCE_PRIVILEGES is on.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.get |
| UserID | Integer | Admin only | Account to act on when calling with admin authentication and Access=admin. Ignored under user authentication (v5.9.6, #2775) |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignStatus | String | No | Filter by status (Draft, Ready, Scheduled, Sending, Sent, Paused, Failed, All) |
| ScheduleType | String/Array | No | Filter by schedule type (Not Scheduled, Immediate, Future, Recursive) |
| SearchKeyword | String | No | Search campaigns by name or email subject (LIKE query) |
| FilterByUserID | Integer | No | Ignored (v5.9.3, #2309). campaigns.get is a user-authenticated endpoint and the listing is always scoped to the authenticated user's own campaigns; any value sent here is overridden with the caller's own user ID. Retained only for backward compatibility. |
| CampaignIDs | String/Array | No | Filter by specific campaign IDs (comma-separated or array) |
| Date_From | String | No | Start date for filtering (YYYY-MM-DD format) |
| Date_To | String | No | End date for filtering (YYYY-MM-DD format) |
| OrderField | String | No | Field to sort by (CampaignName, SendDate, etc.) |
| OrderType | String | No | Sort direction (ASC or DESC) |
| RecordsPerRequest | Integer | No | Number of records per page (0 for all, default: 0) |
| RecordsFrom | Integer | No | Offset for pagination (default: 0) |
| CountOnly | Boolean | No | Return only TotalCampaigns with an empty Campaigns array (default: false). Skips the full result-set fetch and all per-row enrichment; takes precedence over RecordsPerRequest. Use for cheap count/badge calls. |
| RetrieveStatistics | Boolean | No | Include campaign statistics (default: true) |
| RetrieveTags | Boolean | No | Include campaign tags (default: false) |
| Tags | String | No | Comma-separated tag IDs to filter by |
| SplitABTestStatistics | Boolean | No | Include A/B split test statistics (default: false) |
| ExcludeColumns | Array | No | Column names to exclude from SELECT for performance |
| Include_AutoResend | Boolean | No | Include auto-resend campaigns (default: false) |
| IncludeTotalRecipients | Boolean | No | Include aggregate sums over the filtered window: TotalRecipients, TotalSent, TotalDelivered, TotalFailed, TotalOpens, UniqueOpens, TotalClicks, UniqueClicks, TotalHardBounces, TotalSoftBounces, TotalUnsubscriptions (default: false) |
| RetrieveSetupMeta | Boolean | No | Opt-in draft setup metadata for the Campaigns "Draft" tab readiness checklist (default: false). When absent the response is unchanged. When true, each row in Campaigns[] also carries the extra fields listed below. Adds one bulk email query for the page; the main listing query is unchanged. |
When RetrieveSetupMeta is true, each campaign object additionally includes:
| Field | Type | Description |
|---|---|---|
| EmailSubject | String | Subject of the linked email (via RelEmailID); "" when no email is linked. For an A/B draft (RelEmailID == 0) this is the first variation's subject. |
| HasHTMLContent | Boolean | Whether the linked email (or, for A/B, any variation) has a non-empty HTML body. Presence only — the body itself is never returned. |
| HasPlainContent | Boolean | Whether the linked email (or, for A/B, any variation) has a non-empty plain-text body. Presence only. |
| AudienceListCount | Integer | Number of distinct recipient lists in the audience. Derived from RulesJsonBundle, falling back to the legacy campaign_recipients data for legacy drafts. |
| AudienceSegmentCount | Integer | Number of segments in the audience (criteria with an inline rules filter for modern drafts; rows with a segment ID for legacy drafts). |
| HasAudience | Boolean | Convenience flag: AudienceListCount + AudienceSegmentCount > 0. |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaigns.get",
"SessionID": "your-session-id",
"CampaignStatus": "Sent",
"Date_From": "2025-01-01",
"Date_To": "2025-12-31",
"RecordsPerRequest": 50,
"RecordsFrom": 0,
"OrderField": "SendDate",
"OrderType": "DESC",
"RetrieveStatistics": true
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"Campaigns": [
{
"CampaignID": 12345,
"CampaignName": "Summer Sale 2025",
"CampaignStatus": "Sent",
"SendDate": "2025-06-15",
"TotalRecipients": 10000,
"TotalSent": 9950,
"TotalOpens": 3500,
"UniqueOpens": 2100
}
],
"TotalCampaigns": 125
}{
"Success": false,
"ErrorCode": 0
}0: SuccessGet Campaign Status Counts
POST/api.php (legacy)Returns per-status campaign counts in a single GROUP BY query — intended for the Campaign Browse sidebar so it can render all status buckets in one round-trip instead of one campaigns.get call per status. Owner-scoped to the authenticated user.
The response contains two views of the same data: Counts (raw CampaignStatus totals) and Buckets (the sidebar partition, where the single Ready status splits by ScheduleType — Outbox = Sending + Ready/Immediate, Scheduled = Ready/Future|Recursive, Draft = Draft + Ready/Not Scheduled). Counts.Total always equals Buckets.Total.
API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaigns.Get - Legacy endpoint access via
/api.php
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.counts |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| Tags | String | No | CSV of tag IDs to filter by |
| date_from | String | No | Start date (Y-m-d); narrows by SendDate (uniform window across statuses) |
| date_to | String | No | End date (Y-m-d) |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaigns.counts",
"APIKey": "your-api-key",
"date_from": "2026-04-01",
"date_to": "2026-04-30"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"Counts": {
"Draft": 5,
"Ready": 3,
"Sending": 0,
"Paused": 2,
"Pending Approval": 2,
"Sent": 14,
"Failed": 1,
"Total": 27
},
"Buckets": {
"Sent": 14,
"Outbox": 1,
"Draft": 6,
"Scheduled": 1,
"Paused": 2,
"Pending Approval": 2,
"Failed": 1,
"Total": 27
}
}{
"success": "false",
"errors": { "code": 99999, "message": "Not enough privileges" }
}0: SuccessGet Tags List
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tags.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tags.get",
"SessionID": "your-session-id"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"TotalTagCount": 5,
"Tags": [
{
"TagID": 1,
"Tag": "newsletter",
"RelOwnerUserID": 123
},
{
"TagID": 2,
"Tag": "promotion",
"RelOwnerUserID": 123
}
]
}{
"Success": false,
"ErrorCode": 0
}0: SuccessCreate a Tag
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tag.create |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| Tag | String | Yes | Tag name. Allowed characters: letters, numbers, spaces, hyphens, underscores, percent signs (%). Leading and trailing whitespace is trimmed automatically. |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tag.create",
"SessionID": "your-session-id",
"Tag": "summer-campaign"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"TagID": 15
}{
"Success": false,
"ErrorCode": 2
}0: Success
1: Missing required parameter (Tag)
2: Tag already exists in the system
3: Invalid tag format (only letters, numbers, spaces, hyphens and underscores allowed, and percent signs)
4: Tag cannot be empty after trimming whitespaceUpdate a Tag
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tag.update |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| TagID | Integer | Yes | ID of the tag to update |
| Tag | String | Yes | New tag name. Allowed characters: letters, numbers, spaces, hyphens, underscores, percent signs (%). Leading and trailing whitespace is trimmed automatically. |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tag.update",
"SessionID": "your-session-id",
"TagID": 15,
"Tag": "summer-promotion"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": ""
}{
"Success": false,
"ErrorCode": [1, 2]
}0: Success
1: Missing required parameter (TagID)
2: Missing required parameter (Tag)
3: Invalid tag format (only letters, numbers, spaces, hyphens and underscores allowed, and percent signs)
4: Tag cannot be empty after trimming whitespaceDelete Tags
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tags.delete |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| Tags | String | Yes | Comma-separated list of tag IDs to delete |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tags.delete",
"SessionID": "your-session-id",
"Tags": "15,16,17"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": ""
}{
"Success": false,
"ErrorCode": [1]
}0: Success
1: Missing required parameter (Tags)Assign Tag to Campaigns
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Required permissions:
Campaigns.Get - Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tag.assigntocampaigns |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| TagID | Integer | Yes | ID of the tag to assign |
| CampaignIDs | String | Yes | Comma-separated list of campaign IDs |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tag.assigntocampaigns",
"SessionID": "your-session-id",
"TagID": 15,
"CampaignIDs": "100,101,102"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": ""
}{
"Success": false,
"ErrorCode": [1, 2]
}0: Success
1: Missing required parameter (TagID)
2: Missing required parameter (CampaignIDs)Unassign Tag from Campaigns
POST/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Required permissions:
Campaigns.Get - Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: tag.unassignfromcampaigns |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| TagID | Integer | Yes | ID of the tag to unassign |
| CampaignIDs | String | Yes | Comma-separated list of campaign IDs |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "tag.unassignfromcampaigns",
"SessionID": "your-session-id",
"TagID": 15,
"CampaignIDs": "100,101,102"
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": ""
}{
"Success": false,
"ErrorCode": [1, 2]
}0: Success
1: Missing required parameter (TagID)
2: Missing required parameter (CampaignIDs)Create an A/B Split Test Campaign
A/B split test campaigns allow you to send different email variations to segments of your audience and compare performance. The system randomly distributes recipients across variations based on configurable weights.
How A/B Split Testing Works
- Create a campaign using
Campaign.Create - Create email variations — one
Email.Create+Email.Updatecall per variation (minimum 2, maximum 5) - Configure the campaign using
Campaign.UpdatewithABTesting[Variations], audience rules, and schedule - The system handles the rest — during delivery, recipients are randomly assigned to variations based on distribution weights
When the campaign is sent, each recipient receives exactly one email variation. Statistics (opens, clicks, conversions, unsubscriptions, revenue) are tracked per variation, allowing you to compare performance.
Step-by-Step Example
# Create the campaign shell
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Campaign.Create' \
-d 'APIKey=your-api-key' \
-d 'CampaignName=A/B Test: Subject Line Comparison'
# Response: {"Success": true, "ErrorCode": 0, "CampaignID": 200}# --- Variation A ---
# Create the first email
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Email.Create' \
-d 'APIKey=your-api-key'
# Response: {"Success": true, "ErrorCode": 0, "EmailID": 301}
# Set the content for Variation A
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Email.Update' \
-d 'APIKey=your-api-key' \
-d 'EmailID=301' \
-d 'ValidateScope=Campaign' \
-d 'EmailName=Variation A - Discount Subject' \
-d 'Subject=Save 50% Today Only!' \
-d 'FromName=My Store' \
-d 'FromEmail=deals@mystore.com' \
-d 'ReplyToName=My Store' \
-d 'ReplyToEmail=deals@mystore.com' \
-d 'Mode=Editor' \
-d 'HTMLContent=<html><body><h1>Half Price Sale!</h1><p>...</p><p><a href="%Link:Unsubscribe%">Unsubscribe</a></p></body></html>'
# --- Variation B ---
# Create the second email
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Email.Create' \
-d 'APIKey=your-api-key'
# Response: {"Success": true, "ErrorCode": 0, "EmailID": 302}
# Set the content for Variation B
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Email.Update' \
-d 'APIKey=your-api-key' \
-d 'EmailID=302' \
-d 'ValidateScope=Campaign' \
-d 'EmailName=Variation B - Urgency Subject' \
-d 'Subject=Last Chance: Sale Ends at Midnight' \
-d 'FromName=My Store' \
-d 'FromEmail=deals@mystore.com' \
-d 'ReplyToName=My Store' \
-d 'ReplyToEmail=deals@mystore.com' \
-d 'Mode=Editor' \
-d 'HTMLContent=<html><body><h1>Sale Ending Soon!</h1><p>...</p><p><a href="%Link:Unsubscribe%">Unsubscribe</a></p></body></html>'# Update the campaign with A/B testing, audience, and schedule
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Campaign.Update' \
-d 'APIKey=your-api-key' \
-d 'CampaignID=200' \
-d 'CampaignStatus=Ready' \
-d 'ScheduleType=Immediate' \
-d 'RulesJsonBundle={"operator":"and","criteria":[{"list_id":1,"operator":"and","rules":[[{"type":"fields","field_id":"EmailAddress","operator":"is_not_empty","value":""}]]}]}' \
-d 'ABTesting[Variations][0][emailid]=301' \
-d 'ABTesting[Variations][0][weight]=1' \
-d 'ABTesting[Variations][1][emailid]=302' \
-d 'ABTesting[Variations][1][weight]=1'
# Response: {"Success": true, "ErrorCode": 0}
# With equal weights of 1, each variation receives ~50% of recipients.# Retrieve campaign with A/B test statistics
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Campaign.Get' \
-d 'APIKey=your-api-key' \
-d 'CampaignID=200' \
-d 'RetrieveStatistics=true' \
-d 'SplitABTestStatistics=true'
# The response includes per-variation stats inside Campaign.Options.ABTesting.Variations:
# Each variation object includes: EmailID, Weight, Distribution,
# TotalRecipient, TotalSent, TotalFailed, UniqueOpens, UniqueClicks,
# TotalRevenue, TotalUnsubscriptionsUnequal Weight Distribution
You can assign different weights to send more traffic to a preferred variation:
# 75% to Variation A, 25% to Variation B
-d 'ABTesting[Variations][0][emailid]=301'
-d 'ABTesting[Variations][0][weight]=3'
-d 'ABTesting[Variations][1][emailid]=302'
-d 'ABTesting[Variations][1][weight]=1'
# Three variations: 50% / 25% / 25%
-d 'ABTesting[Variations][0][emailid]=301'
-d 'ABTesting[Variations][0][weight]=2'
-d 'ABTesting[Variations][1][emailid]=302'
-d 'ABTesting[Variations][1][weight]=1'
-d 'ABTesting[Variations][2][emailid]=303'
-d 'ABTesting[Variations][2][weight]=1'Retrieving A/B Test Statistics
When retrieving a campaign with SplitABTestStatistics=true, the response includes per-variation performance metrics:
{
"Success": true,
"Campaign": {
"CampaignID": 200,
"CampaignName": "A/B Test: Subject Line Comparison",
"CampaignStatus": "Sent",
"RelEmailID": 0,
"Options": {
"ABTesting": {
"Variations": [
{
"EmailID": 301,
"Weight": 1,
"Distribution": 50,
"TotalRecipient": 5000,
"TotalSent": 4980,
"TotalFailed": 20,
"UniqueOpens": 1200,
"UniqueClicks": 350,
"TotalRevenue": 15000,
"TotalUnsubscriptions": 5
},
{
"EmailID": 302,
"Weight": 1,
"Distribution": 50,
"TotalRecipient": 5000,
"TotalSent": 4975,
"TotalFailed": 25,
"UniqueOpens": 1450,
"UniqueClicks": 420,
"TotalRevenue": 18500,
"TotalUnsubscriptions": 3
}
]
}
},
"OpenStatistics": { ... },
"ClickStatistics": { ... }
}
}When SplitABTestStatistics=true, the OpenStatistics, ClickStatistics, ConversionStatistics, and UnsubscriptionStatistics objects also include per-variation breakdowns grouped by RelEmailID.
Disabling A/B Testing
To remove A/B testing from a campaign and revert to a single email, pass an empty ABTesting parameter and set RelEmailID:
curl -X POST https://example.com/api.php \
-d 'ResponseFormat=JSON' \
-d 'Command=Campaign.Update' \
-d 'APIKey=your-api-key' \
-d 'CampaignID=200' \
-d 'ABTesting=' \
-d 'RelEmailID=301'Copying an A/B Test Campaign
Use Campaign.Copy to duplicate an A/B test campaign. All email variations are automatically duplicated with new Email IDs, and the A/B testing configuration is preserved in the copy.
Create a Split Test (Legacy)
Legacy API
This endpoint uses the older split test system where a test portion of recipients receive different email variations, the campaign pauses to wait for results, then the winning variation is sent to the remaining audience. For the modern A/B testing approach where all recipients are distributed across variations simultaneously, see Create an A/B Split Test Campaign above.
/api.php (legacy)API Usage Notes
- Authentication required: User API Key
- Required permissions:
Campaign.Create - Legacy endpoint access via
/api.phponly (no v1 REST alias configured)
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: splittest.create |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | ID of the campaign to create split test for |
| TestSize | Integer | Yes | Percentage of recipients to include in test (e.g., 20 for 20%) |
| TestDuration | Integer | Yes | Duration in seconds to wait before selecting the winner |
| Winner | String | Yes | Winner criteria: Highest Open Rate or Most Unique Clicks |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "splittest.create",
"SessionID": "your-session-id",
"CampaignID": 12345,
"TestSize": 20,
"TestDuration": 86400,
"Winner": "Highest Open Rate"
}'{
"Success": true,
"ErrorCode": 0,
"SplitTestID": 567
}{
"Success": false,
"ErrorCode": [1, 2]
}0: Success
1: Missing required parameter (CampaignID)
2: Missing required parameter (TestSize)
4: Missing required parameter (TestDuration)
5: Missing required parameter (Winner)
6: Campaign not found or doesn't belong to userExport Campaigns to CSV
POST/api/v1/campaigns.exportEnqueues a background job that exports the campaigns list (past sends plus engagement aggregates) to a CSV file. Accepts the same filter shape as Get Campaigns List, so the export matches the equivalent campaigns.get query row-for-row (subject to the selected column allow-list). Returns a JobID to poll with Get Campaign Export Status.
API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaigns.Get - Rate limit: 100 requests per 60 seconds
- Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.export.post |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| ExportFormat | String | No | Output format. Possible values: csv. (json is reserved for a future release and currently rejected.) Defaults to csv. |
| FieldsToExport | Array | No | Columns to include, from the allow-list (see below). Defaults to a standard column set when omitted. |
| CampaignStatus | String | No | Filter by status (Draft, Ready, Scheduled, Sending, Sent, Paused, Failed, All) |
| ScheduleType | String/Array | No | Filter by schedule type (Not Scheduled, Immediate, Future, Recursive) |
| SearchKeyword | String | No | Search campaigns by name or email subject (LIKE query) |
| CampaignIDs | String/Array | No | Filter by specific campaign IDs (comma-separated or array) |
| Tags | String | No | Comma-separated tag IDs to filter by |
| Date_From | String | No | Start date for filtering (YYYY-MM-DD format) |
| Date_To | String | No | End date for filtering (YYYY-MM-DD format) |
| FilterByUserID | Integer | No | Filter by account/user ID (admin only) |
| Include_AutoResend | Boolean | No | Include auto-resend campaigns (default: false) |
| SplitABTestStatistics | Boolean | No | Emit one row per A/B variation under each parent campaign (default: false) |
Allowed FieldsToExport values: CampaignID, CampaignName, EmailSubject, CampaignStatus, ScheduleType, SendDate, SendTime, CreateDateTime, SendProcessStartedOn, SendProcessFinishedOn, TotalRecipients, TotalSent, TotalFailed, TotalDelivered, TotalOpens, UniqueOpens, TotalClicks, UniqueClicks, TotalConversions, UniqueConversions, TotalForwards, UniqueForwards, TotalViewsOnBrowser, UniqueViewsOnBrowser, TotalUnsubscriptions, TotalHardBounces, TotalSoftBounces, TotalRevenue, RecipientLists, RecipientSegments.
curl -X POST https://example.com/api/v1/campaigns.export \
-H "Content-Type: application/json" \
-d '{
"Command": "campaigns.export.post",
"SessionID": "your-session-id",
"CampaignStatus": "Sent",
"Tags": "5,12",
"Date_From": "2026-04-01",
"Date_To": "2026-04-30",
"ExportFormat": "csv",
"FieldsToExport": ["CampaignID", "CampaignName", "EmailSubject", "SendProcessFinishedOn", "TotalRecipients", "TotalSent", "TotalDelivered", "UniqueOpens", "UniqueClicks", "TotalHardBounces", "TotalSoftBounces", "TotalUnsubscriptions"],
"SplitABTestStatistics": false,
"Include_AutoResend": true
}'{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"JobID": 8421,
"ExportID": 8421,
"EstimatedRows": 173
}{
"Errors": [
{ "Code": 1, "Message": "Invalid ExportFormat (only \"csv\" is currently supported)" }
]
}0: Success
1: Invalid ExportFormat (only "csv" is currently supported)
2: FieldsToExport must be a non-empty array
3: FieldsToExport contains unknown field(s)Get Campaign Export Status
GET/api/v1/campaigns.exportPolls the status of a campaign export job created by Export Campaigns to CSV, or streams the produced file. When Download=1 and the job is finished, the endpoint streams the CSV directly with explicit Content-Type: text/csv and Content-Disposition: attachment headers (it does not return JSON in that case).
API Usage Notes
- Authentication is done by User API Key
- Required permissions:
Campaigns.Get - Rate limit: 100 requests per 60 seconds
- A job can only be accessed by its owner, and only as a
CampaignExportjob (aSubscriberExport/JourneyExportID owned by the same user returns "not found") - Legacy endpoint access via
/api.phpis also supported
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaigns.export.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| JobID | Integer | Yes | The export job ID returned by campaigns.export.post (alias: ExportID) |
| Download | Boolean | No | When 1 and Status=Finished, streams the CSV file instead of returning the JSON envelope |
# Poll status
curl -X GET "https://example.com/api/v1/campaigns.export?Command=campaigns.export.get&SessionID=your-session-id&JobID=8421"
# Download the finished file
curl -X GET "https://example.com/api/v1/campaigns.export?Command=campaigns.export.get&SessionID=your-session-id&JobID=8421&Download=1" -o campaigns.csv{
"Success": true,
"ErrorCode": 0,
"ErrorText": "",
"JobID": 8421,
"Status": "Finished",
"Progress": 100,
"RowCount": 173,
"DownloadURL": "api.php?Command=campaigns.export.get&jobid=8421&download=1",
"DownloadSize": 28456,
"ExpiresAt": "2026-05-07 15:37:58"
}{
"Errors": [
{ "Code": 5, "Message": "Export job is not completed yet" }
]
}0: Success
1: Missing JobID parameter
2: Invalid JobID parameter (not a positive integer)
3: Export job not found (wrong owner or never existed)
4: Export job not found (job exists but is not a CampaignExport)
5: Download requested but the job is not finished yetGet Campaign Link Clicks
GET/api/v1/campaign.linkclicks.getAPI Usage Notes
- Authentication required: User API Key (or Admin API Key)
- Required permissions:
Campaign.Get - v1 REST alias:
GET /api/v1/campaign.linkclicks.get. Legacy access via/api.phpis also supported
Returns the per-link click ranking ("most clicked links") of a sent campaign, or the per-subscriber click breakdown ("who clicked"), depending on GroupBy. Results are ordered by click count (descending) and paginated. Automated/bot clicks are excluded (IsAutomated=0), so the numbers match the bundled campaign report. Access is owner-scoped; admins (via Admin API Key) may read any campaign.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.linkclicks.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | The campaign to report on |
| GroupBy | String | No | Grouping mode. Possible values: Links (default), Subscribers |
| RecordsFrom | Integer | No | Pagination offset (default: 0) |
| RecordsPerRequest | Integer | No | Page size (default: 25, hard cap: 1000) |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.linkclicks.get",
"APIKey": "your-api-key",
"CampaignID": 7524,
"GroupBy": "Links",
"RecordsPerRequest": 25
}'{
"Success": true,
"ErrorCode": 0,
"CampaignID": 7524,
"GroupBy": "Links",
"TotalRecords": 12,
"Links": [
{
"LinkTitle": "Read more",
"LinkURL": "https://example.com/landing",
"TotalClicks": 2771
}
]
}{
"Success": false,
"ErrorCode": 3,
"ErrorMessage": "Campaign not found or access denied"
}0: Success
2: CampaignID is not numeric
3: Campaign not found or access deniedWhen GroupBy is Subscribers, the response returns a Subscribers array instead of Links, where each row is { "SubscriberID": 62362, "ListID": 47, "EmailAddress": "user@example.com", "TotalClicks": 3 }. Seed-list recipients (SubscriberID 0) are returned with an empty EmailAddress.
Get Campaign Recipients Activity
GET/api/v1/campaign.recipients.activity.getAPI Usage Notes
- Authentication required: User API Key (or Admin API Key)
- Required permissions:
Campaign.Get - v1 REST alias:
GET /api/v1/campaign.recipients.activity.get. Legacy access via/api.phpis also supported
For a sent campaign, returns the per-subscriber engagement breakdown for a single activity type ("who opened", "who clicked", "who bounced", etc.), read from the MySQL oempro_stats_* tables. Results are grouped per subscriber (one row per subscriber/list), ordered by most recent activity, and paginated. This is the sent-campaign counterpart to campaign.recipients.get, which only previews the audience of unsent campaigns. Automated/bot opens and clicks are excluded (IsAutomated=0). Access is owner-scoped.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.recipients.activity.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | The campaign to report on |
| Activity | String | Yes | Activity type. Possible values: open, click, bounce, unsubscription, forward, conversion |
| SearchKeyword | String | No | Case-insensitive substring filter on the recipient's email address (LIKE %keyword%), applied before pagination so TotalRecords reflects the filtered count. Empty/absent = no filter. EmailAddress is accepted as an alias. |
| VariationEmailID | Integer | No | New in v6.0.1 Limits the rows to recipients of one A/B variation. The value is a variation's EmailID from campaign.abtest.get (ABTest.Variations[].EmailID). Applied before pagination, so TotalRecords is the filtered count. Can be combined with SearchKeyword. Empty or absent means no filter. |
| RecordsFrom | Integer | No | Pagination offset (default: 0) |
| RecordsPerRequest | Integer | No | Page size (default: 25, hard cap: 1000) |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.recipients.activity.get",
"APIKey": "your-api-key",
"CampaignID": 7524,
"Activity": "click",
"RecordsPerRequest": 25
}'{
"Success": true,
"ErrorCode": 0,
"CampaignID": 7524,
"Activity": "click",
"TotalRecords": 1488,
"Recipients": [
{
"SubscriberID": 62362,
"ListID": 47,
"ListName": "Buyers List",
"EmailAddress": "user@example.com",
"ActivityCount": 2,
"LastActivityDate": "2026-05-28 00:33:36",
"VariationEmailID": 0
}
]
}{
"Success": false,
"ErrorCode": 4,
"ErrorMessage": "Invalid activity. Allowed: open, click, bounce, unsubscription, forward, conversion"
}0: Success
2: CampaignID is not numeric
3: Campaign not found or access denied
4: Invalid activity type
5: VariationEmailID is not a positive integer
6: VariationEmailID is not an A/B variation of this campaign (or the campaign is not an A/B campaign)
7: Variation filter unavailable: some rows of this activity do not record their variation and the campaign's send queue has been purged, or the variation data could not be read
8: Recipient activity could not be retrieved (a database query failed)For bounce, each recipient row additionally includes a BounceType field (Hard or Soft). For conversion, each row additionally includes TotalRevenue (decimal string; stored internally as integer cents).
Each row carries ListName (the subscriber's list name) alongside ListID. When SearchKeyword (or its EmailAddress alias) is supplied, the email match is performed server-side across every list the campaign targeted, before pagination — so TotalRecords is the filtered total and RecordsFrom/RecordsPerRequest page through the matches. Seed-list recipients (SubscriberID 0) have no email address and are therefore excluded from search results.
VariationEmailID, the EmailID of the A/B variation sent to that recipient. It is 0 for a campaign that is not an A/B campaign, or when the variation cannot be determined. When VariationEmailID is passed in the request, every row carries that value.Open, click and forward activity records the variation the recipient interacted with. Some bounce, unsubscription and conversion records do not, for example bounces processed from DSN messages and unsubscriptions from spam complaints. For those, the variation comes from the campaign's send queue, which records the variation sent to every recipient. The send queue is removed RETENTION_DAYS_CAMPAIGN_QUEUE days after the campaign was created (default 250). After that, rows that did not record their variation report VariationEmailID 0, and filtering that activity by variation returns error 7 rather than a partial list. Filtering open, click and forward activity keeps working. Seed-list recipients are included in a variation filter only when their activity row recorded the variation.
8 (or 7 with a variation filter). Before v6.0.1 it returned Success with zero rows.Get Campaign A/B Test & Auto-Resend Uplift
GET/api/v1/campaign.abtest.getAPI Usage Notes
- Authentication required: User API Key (or Admin API Key)
- Required permissions:
Campaign.Get - v1 REST alias:
GET /api/v1/campaign.abtest.get. Legacy access via/api.phpis also supported
Exposes the per-variation A/B (MVT) statistics and the auto-resend uplift metrics that the bundled campaign report assembles. Per-variation metrics exclude seed-list recipients so the numbers reflect the weighted audience split. The response always contains ABTest, AutoResend, and ParentCampaign keys; AutoResend and ParentCampaign are null when not applicable, and ABTest.IsABTest is false for non-A/B campaigns. Access is owner-scoped.
Request Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| Command | String | Yes | API command: campaign.abtest.get |
| SessionID | String | No | Session ID obtained from login |
| APIKey | String | No | API key for authentication |
| CampaignID | Integer | Yes | The campaign to report on |
curl -X POST https://example.com/api.php \
-H "Content-Type: application/json" \
-d '{
"Command": "campaign.abtest.get",
"APIKey": "your-api-key",
"CampaignID": 7524
}'{
"Success": true,
"ErrorCode": 0,
"CampaignID": 7524,
"ABTest": {
"IsABTest": true,
"Variations": [
{
"EmailID": 9001,
"EmailName": "Variation A",
"Subject": "Subject A",
"FromName": "Acme",
"FromEmail": "news@acme.com",
"ContentType": "HTML",
"TotalRecipients": 5000,
"TotalSent": 4980,
"TotalFailed": 20,
"UniqueOpens": 1200,
"UniqueClicks": 300,
"Unsubscriptions": 5,
"Revenue": "120.00",
"OpenRate": 24.1,
"ClickRate": 6,
"CTOR": 25
}
],
"Winner": {
"EmailID": 9001,
"EmailName": "Variation A"
}
},
"AutoResend": {
"Status": "completed",
"CampaignID": 7600,
"CampaignName": "Resend to non-openers",
"TotalSent": 3000,
"UniqueOpens": 400,
"UniqueClicks": 90,
"OpenRate": 13.3,
"ClickRate": 3,
"OpenUplift": 33.3,
"ClickUplift": 30
},
"ParentCampaign": null
}{
"Success": false,
"ErrorCode": 3,
"ErrorMessage": "Campaign not found or access denied"
}0: Success
2: CampaignID is not numeric
3: Campaign not found or access deniedAutoResend.Status is completed when the auto-resend child campaign has been created (uplift fields are populated relative to this parent), or scheduled when auto-resend is enabled but not yet sent (in which case it carries WaitDays, Subject, and PreHeaderText). When the requested campaign is itself an auto-resend child, ParentCampaign is populated with the parent's headline metrics and CTOR.

