Bulk Messaging

7 views Markdown

The six endpoints that send one message to many: choosing the audience, counting it, testing and sending.

Overview

These six endpoints run every step of sending one message to many people: choosing who to target, seeing how many that is, trying it on yourself, and sending it.

The audience comes from one of two places. Filters pick clients by group, country or the products they hold. A newsletter list is a hand-kept list of addresses whose members need not be clients. Give a newsletter key and the filters are ignored.

Sending is not immediate: the messages go into the notification queue and are worked through in the background. The batch id on the response is how you find that batch's rows in the queue.

Reference

Reading the Filter Options

get/api/v1/admin/tools/bulk/lookups
Tools/GetBulkLookups admin where the filters come from

Returns the values you can use in the recipient filters.

Response fields data — 7
user_groupsarrayThe client groups.
departmentsarrayThe departments.
countriesobject[]The countries, each with an id, a name and a country code.
languagesobject[]The languages, each with a key and a name.
tldsarrayThe domain extensions.
productsarrayThe products, with their category tree.
service_statusesstring[]The service statuses.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/tools/bulk/lookups' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/tools/bulk/lookups', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tools/bulk/lookups');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Take the filter values from HERE: guessing ids quietly produces an empty audience.
$lookups = Api::Tools()->GetBulkLookups()['data'];

$groupIds = array_column($lookups['user_groups'], 'id');

Counting the Recipients

post/api/v1/admin/tools/bulk/contacts
Tools/GetBulkContacts admin sends nothing

Tells you how many people your filters reach, without sending anything.

Body — who 4
user_typestringThe audience: client or staff. It defaults to clients.
notification_typestringThe channel: mail or sms. The channel changes who counts as reachable.
newsletterstringA newsletter key. Give it and the newsletter list is used instead of the filters.
fullintAlso returns the recipients themselves, alongside the count.
Body — filters 11
user_groupsarrayFilters by client group.
departmentsarrayFilters by department.
countriesarrayFilters by country.
languagesarrayFilters by language.
servicesarrayFilters by the products held.
serversarrayFilters by the servers services sit on.
addonsarrayFilters by the add-ons held.
services_statusarrayFilters by service status.
client_statusarrayFilters by client status.
without_productsintTargets clients with no product at all.
birthday_marketingintTargets clients whose birthday it is.
Response fields data — 3
countintHow many recipients matched.
contactsarrayThe recipients. Filled only when you asked for them.
sourcestringWhere the recipients came from: the filters or the newsletter.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tools/bulk/contacts' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"user_type":"client","notification_type":"mail","user_groups":[1]}'
const res = await fetch('https://panel.example.com/api/v1/admin/tools/bulk/contacts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    user_type: 'client',
    notification_type: 'mail',
    user_groups: [1],
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tools/bulk/contacts');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'user_type'         => 'client',
        'notification_type' => 'mail',
        'user_groups'       => [1],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Count BEFORE sending: give the same body to the submit endpoint and that many people get it.
$audience = ['user_type' => 'client', 'user_groups' => [1]];
$count    = Api::Tools()->GetBulkContacts($audience)['data']['count'];

if ($count > 0 && $count < 5000) {
    Api::Tools()->SubmitBulkNotification($audience + [
        'subject' => 'Notice',
        'message' => $html,
    ]);
}

Sending a Test

post/api/v1/admin/tools/bulk/test
Tools/TestBulkNotification admin goes only to you

Sends the message to the addresses you give and to department staff, not to clients.

Body 5
messagestringrequiredThe message body.
notification_typestringThe channel. It defaults to e-mail.
subjectstringThe subject. Left empty, a fixed heading is used.
departmentsint[]The departments. It goes to the staff assigned to them.
emailsstringOutside addresses. One per line.
Response fields data — 1
sentintHow many tests went out.
Errors 2
body_required422The message body was empty.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tools/bulk/test' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"subject":"Test","message":"Hello.","emails":"[email protected]"}'
const res = await fetch('https://panel.example.com/api/v1/admin/tools/bulk/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    subject: 'Test',
    message: 'Hello.',
    emails: '[email protected]',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tools/bulk/test');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'subject' => 'Test',
        'message' => $html,
        'emails'  => '[email protected]',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The test ignores the FILTERS: it checks how the message looks, not who it would reach.
Api::Tools()->TestBulkNotification([
    'subject' => 'Notice',
    'message' => $html,
    'emails'  => '[email protected]',
]);

Sending the Bulk Notification

post/api/v1/admin/tools/bulk/submit
Tools/SubmitBulkNotification admin cannot be recalled

Queues the message for everyone who matched. This is the real send.

Body — the message 5
messagestringrequiredThe message body.
subjectstringThe subject. Required on e-mail.
notification_typestringThe channel. It defaults to e-mail.
ccstringAddresses to copy in. One per line.
deliverystringThe envelope: single one message each, multiple all in one envelope. In one envelope the recipients can see each other.
Body — who 3
user_typestringThe audience: clients or staff.
newsletterstringA newsletter key. Give it and the newsletter list is used instead of the filters.
*array | intThe same eleven filters as on the counting endpoint apply here too.
Response fields data — 3
queuedboolWhether it was queued.
recipientsintHow many recipients were queued.
batch_idstringThe id of this send. It is how you find this send's rows in the notification queue.
Errors 4
subject_required422The subject was empty on an e-mail.
body_required422The message body was empty.
no_recipients422No recipient matched the filters.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tools/bulk/submit' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"notification_type":"mail","user_type":"client","subject":"Notice","message":"Important update.","delivery":"single","user_groups":[1]}'
const res = await fetch('https://panel.example.com/api/v1/admin/tools/bulk/submit', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    notification_type: 'mail',
    user_type: 'client',
    subject: 'Notice',
    message: 'Important update.',
    delivery: 'single',
    user_groups: [1],
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tools/bulk/submit');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'subject'     => 'Notice',
        'message'     => $html,
        'delivery'    => 'single',
        'user_groups' => [1],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A queued send CANNOT be recalled; cancelling in bulk means deleting the queue rows.
$batch = Api::Tools()->SubmitBulkNotification($audience + [
    'subject' => 'Notice',
    'message' => $html,
])['data'];

// It went out wrong: pull the queued ones back.
$queued = Api::Tools()->GetNotificationQueue([], ['status' => 'pending'])['data'];
$ids    = array_column(
    array_filter($queued, fn (array $n): bool => $n['batch_id'] === $batch['batch_id']),
    'id',
);

Api::Tools()->BulkDeleteNotificationQueue(['ids' => $ids]);
Response
{
  "data": {
    "queued": true,
    "recipients": 1842,
    "batch_id": "b7f3c1a9"
  }
}
{
  "error": {
    "code": "no_recipients",
    "message": "No recipient matched the filters."
  }
}

Reading a Newsletter List

get/api/v1/admin/tools/bulk/newsletters
Tools/GetBulkNewsletters admin per language

Returns the newsletter subscribers, who need not be clients at all.

Query parameters 2
typestringThe channel: email or sms. It defaults to e-mail.
langstringA language code. Leave it out and every language comes back as a summary, in a different response shape.
Response fields data
entriesstring[]The addresses or numbers on the list. Returned when a language is given.
countintHow many records are on the list.
by_langobjectThe count and the list keyed by language. This comes back instead when no language is given.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/tools/bulk/newsletters' \
  -H "Authorization: Bearer $API_KEY" \
  -d type=email \
  -d lang=en
const url = new URL('https://panel.example.com/api/v1/admin/tools/bulk/newsletters');
url.searchParams.set('type', 'email');
url.searchParams.set('lang', 'en');

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();
$url = 'https://panel.example.com/api/v1/admin/tools/bulk/newsletters?' . http_build_query([
    'type' => 'email',
    'lang' => 'en',
]);

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The response CHANGES SHAPE with the language: give one and you get a flat list, omit it and you get a summary per language.
$one = Api::Tools()->GetBulkNewsletters([], ['lang' => 'en'])['data']['entries'];
$all = Api::Tools()->GetBulkNewsletters()['data']['by_lang'];

Writing a Newsletter List

put/api/v1/admin/tools/bulk/newsletters
Tools/SaveBulkNewsletters admin the list is written whole

Rewrites the newsletter list for one language and channel.

Body 4
langstringrequiredWhich language's list to write.
typestringThe channel. It defaults to e-mail.
entriesstring[]The whole list. What you send becomes the truth.
contentstringThe same list as text, one per line. Used instead of the array.
Response fields data — 3
langstringThe language written.
typestringThe channel written.
countintHow many records the list holds.
Errors 2
lang_required422No language was given.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/tools/bulk/newsletters' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"lang":"en","type":"email","entries":["[email protected]","[email protected]"]}'
const res = await fetch('https://panel.example.com/api/v1/admin/tools/bulk/newsletters', {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    lang: 'en',
    type: 'email',
    entries: ['[email protected]', '[email protected]'],
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tools/bulk/newsletters');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'lang'    => 'en',
        'type'    => 'email',
        'entries' => $entries,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// To ADD one address send the existing list too: what you send replaces what was there.
$entries   = Api::Tools()->GetBulkNewsletters([], ['lang' => 'en'])['data']['entries'];
$entries[] = '[email protected]';

Api::Tools()->SaveBulkNewsletters(['lang' => 'en', 'entries' => $entries]);

Pitfalls

A queued send cannot be recalled

The submit endpoint has no undo. The only way to stop a wrong message is to take the batch id from the response, find the pending rows in the notification queue and delete them; whatever already went is gone. That is why counting first is not a formality but the only safety you have.

The test does not verify the audience

The test endpoint never reads the filters: it sends only to the addresses you give and to department staff. So it verifies how the message looks, not who would get it. Verifying the audience is the counting endpoint's job, and the two go together.

One envelope lets recipients see each other

Choosing the shared envelope merges the message into one send. That is faster, but the recipient addresses can become visible to each other, and handing out a client list is a data leak. When in doubt, pick the per-recipient form.

The newsletter list is written whole

The write does not merge: the set you send replaces that language's list. Adding one address means reading the current list and appending to it, or the rest of the subscribers are deleted.

The newsletter read changes shape with the language

Give a language and you get a flat list; omit it and you get a summary grouped per language. Code expecting one shape reads an empty list when the language parameter is forgotten, and takes that for "no subscribers".

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.