Webhook Documentation
What is a Webhook
Webhook is a mechanism that allows your system to receive automatic notifications about events via HTTP requests. When a certain event occurs in our system (for example, a campaign is completed), we send a POST request with detailed information to the URL you specify.
Webhook Setup
You can configure webhooks in the web control panel:
- Go to the Integrations section.
- Click on the Webhooks card.
- Click Create webhook.
- Fill in the fields:
- Webhook Name — for your personal identification of the webhook.
- Webhook URL — the address of your server that will receive notifications.
- Events — select from the list the events you want to be notified about.
- Click Create Webhook.
After creation, the webhook will appear in the general list. Its card displays all key information:
- Status (Active / Inactive)
- Webhook Name and Webhook URL
- List of tracked events
- Time of last successful send
- Activity (total number of events sent)
The following actions are available in the menu (⋮) in the corner of the card:
- Test
- Edit
- Activate / Deactivate
- Delete
Notification Format
{
"eventId": "<uuid>",
"trigger": "<entity>.<event>",
"occurredAt": "2023-10-27T12:35:01.123Z",
"payload": {
"someStringField": "string",
"numberField": 42
}
}
Request fields:
eventId— unique event identifier.trigger— event type inentity.eventformat (see list below).occurredAt— time the event occurred in ISO 8601 format (UTC).payload— object with data specific to each event.
Test Event
When you click the Test button in the menu (⋮) in the corner of the card, you will receive a POST request with the following payload:
{
"eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"trigger": "emailmassivo.webhook_test",
"occurredAt": "2030-12-31T20:59:59.999Z",
"payload": {
"timestamp": "2030-12-31T20:59:59.999Z",
"stringField": "someString",
"numberField": 42,
"booleanField": true,
"objectField": {
"nestedString": "value",
"nestedNumber": 123
},
"arrayField": ["item1", "item2", 3, false]
}
}
Available Triggers
Entity: Email campaigns (mail_distribution)
Common Payload for campaign events
For events mail_distribution.started, mail_distribution.completed, mail_distribution.moderating, mail_distribution.approved and mail_distribution.rejected, the following payload structure is used:
{
"mailDistributionId": 12345,
"mailDistributionName": "Campaign name",
"mailDistributionSubject": "Email subject"
}
mail_distribution.started— campaign has been started (Started).mail_distribution.completed— campaign completed successfully (Completed).mail_distribution.moderating— campaign sent for review (Under Moderation).mail_distribution.approved— campaign approved by moderator (Approved).mail_distribution.rejected— campaign rejected by moderator (Rejected).mail_distribution.banned— campaign is blocked (Stopped by System). This event has an extendedpayload.
Payload:
{
"mailDistributionId": 12345,
"mailDistributionName": "Campaign name",
"mailDistributionSubject": "Email subject",
"negative": {
"reason": "hard_bounced",
"explanation": "Detailed description of the block reason"
}
}
The reason field will contain one of the following strings:
hard_bouncedsoft_bouncederror_spamcomplain
Entity: Transactional emails (email)
Events about transactional emails sent through the API and SMTP. In the webhook form, this is the "Transactional emails — detailed" group. Unlike the earlier transactional email events, they include the email ID, the recipient type, and the bounce reason — we recommend this group for new integrations.
Each event refers to one recipient of the email: if an email went to three addresses, you get events for each of them.
Common Payload
{
"emailId": "0199b2f4-6c1e-7a3d-9f2b-4e8c1d7a5b60",
"recipient": {
"id": "c4a07e13-5d2b-4e8f-b916-7f3a2d0c8b44",
"email": "user@example.com",
"type": "to"
}
}
emailId— the email ID the API returned when sending (data.id).recipient.id— the recipient ID within the email.recipient.email— the recipient's address.recipient.type—tois the primary recipient,cca copy,bcca blind copy.
Events
email.delivered— "Delivered": the recipient's mail server accepted the email.email.opened— "Opened": the recipient opened the email for the first time.email.clicked— "Click": the recipient clicked a link in the email for the first time. Thepayloadalso hasurl— the link they followed.email.unsubscribed— "Unsubscribe": the recipient unsubscribed from emails of this sending key.email.complained— "Complaint": the recipient marked the email as spam.email.bounced— "Bounced": the email wasn't delivered — the address doesn't exist, the mailbox is full, the recipient's server refused it, or the address is already known as undeliverable.email.failed— "Sending Failed": the email couldn't be sent. The recipient's address is fine — you can send the email again.email.suppressed— "Not sent": the recipient had unsubscribed from emails of this sending key or complained about them, so the email wasn't sent.
Bounce reason
Events email.bounced, email.failed and email.suppressed have a reason object in the payload:
{
"emailId": "0199b2f4-6c1e-7a3d-9f2b-4e8c1d7a5b60",
"recipient": {
"id": "c4a07e13-5d2b-4e8f-b916-7f3a2d0c8b44",
"email": "user@example.com",
"type": "to"
},
"reason": {
"code": "DELIVERY_MAILBOX_FULL",
"bounceType": "transient",
"message": "Recipient mailbox is full.",
"smtpResponse": "552 5.2.2 Mailbox full"
}
}
code— the reason code, for exampleDELIVERY_MAILBOX_FULL(mailbox is full) orRECIPIENT_UNSUBSCRIBED(the recipient unsubscribed).bounceType— only inemail.bounced:permanent— the address doesn't exist, remove it from your list;transient— a temporary refusal, the address can still be used;undetermined— the reason is unclear.message— a description of the reason.smtpResponse— the response of the recipient's mail server, if there is one.
Important! Remove an address from your list only after
email.bouncedwithbounceType: permanent.
Occasionally a mail server reports non-delivery after it has accepted the email. Then email.bounced follows email.delivered for the same recipient.
All reason codes are in the developer documentation.
Processing Requirements
1. Server response and timeout
Your server must respond to the request within 30 seconds.
- HTTP status
200–299: means the event was successfully received and processed. - Any other status (
3xx,4xx,5xx) or timeout: is considered an error, after which we will attempt to deliver the event again.
2. Idempotency
Our system guarantees delivery on an at-least-once basis. This means that due to network issues or errors on your server side, the same event may be delivered more than once. Use the eventId field to prevent duplicate processing.
3. Retries
In case of delivery failure we will make 6 retry attempts with increasing intervals.
| Attempt | Delay after previous |
|---|---|
| 1 | 1 minute |
| 2 | 2 minutes |
| 3 | 4 minutes |
| 4 | 8 minutes |
| 5 | 15 minutes |
| 6 | 15 minutes |
In total we may send up to 7 requests for one event (initial send + 6 retries). After the last failed attempt we will stop delivering the event and it will be marked as undelivered.
4. URL requirements
Your URL handler must use the HTTPS protocol and have a valid SSL certificate. Notifications to HTTP addresses will not be delivered.
Handler Example
PHP
<?php
// Get raw request body
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data || !isset($data['eventId'])) {
http_response_code(400); // Bad Request
exit('Invalid payload');
}
// Check if event was already processed
if (isEventProcessed($data['eventId'])) {
http_response_code(200);
exit('OK. Already processed.');
}
// Process the event
switch ($data['trigger']) {
case 'mail_distribution.completed':
handleMailDistributionCompleted($data['payload']);
break;
case 'mail_distribution.banned':
handleMailDistributionBanned($data['payload']);
break;
// ... other events
}
// Mark event as processed
markEventAsProcessed($data['eventId']);
http_response_code(200);
echo 'OK';
?>
Node.js (Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
const { eventId, trigger, payload } = req.body;
// Check idempotency
if (isEventProcessed(eventId)) {
return res.status(200).send('OK. Already processed.');
}
// Process the event
switch (trigger) {
case 'mail_distribution.completed':
handleMailDistributionCompleted(payload);
break;
case 'mail_distribution.banned':
handleMailDistributionBanned(payload);
break;
// ... other events
}
// Mark as processed
markEventAsProcessed(eventId);
res.status(200).send('OK');
});
Development Recommendations
1. Process requests asynchronously
Your server should respond with HTTP 200 OK as quickly as possible. Do not perform long-running operations (calls to other APIs, file processing) at the moment of receiving the webhook. Instead, add the task to a queue (e.g. RabbitMQ, Redis, SQS) and return the response immediately. This will protect you from timeouts and make it easy to scale processing.
2. Be prepared for new fields
New fields may be added to the payload object in the future. Your code should be resilient to this and not fail if unknown keys appear in the JSON.
3. Set up monitoring
Set up monitoring for your URL handler. Alerts will help you react to issues quickly and fix them before events start being lost after all retry attempts.
4. Use test environments
Do not use your main (production) URL for debugging. Create a separate webhook for a test or staging environment to safely make changes to the code.
Frequently Asked Questions (FAQ)
-
I've set up a webhook but I'm not receiving notifications. What should I do?
Check the following:
- Status in UI: Make sure the webhook is Active.
- Test event: Use the Test button on the webhook card.
- Public URL: Make sure the URL is accessible from the internet (not
localhost). - HTTPS: Make sure the URL starts with
https://and has a valid SSL certificate. - Server logs: Check your web server (Nginx, Apache) or application logs for incoming request entries.
-
Why do you send the same event (
eventId) multiple times?We repeat the send if we do not receive confirmation of successful delivery from your server (response with HTTP code 200–299) within 30 seconds. This can happen for two main reasons:
- Your server returned an error (e.g. status
500 Internal Server Error). - A network issue occurred, due to which we did not receive the response, even if your application sent it and successfully processed the event.
Therefore your system should always be ready for duplicates (see the Idempotency section).
- Your server returned an error (e.g. status
-
Is the order of event delivery guaranteed?
No, it is not. Due to network behaviour or retry logic, an event that occurred later may be delivered earlier. Always rely on the
occurredAttimestamp in the request body, not on the order in which they are received. -
What happens if my server is unavailable for a long time?
We will attempt to deliver the event for approximately 45 minutes. If your server remains unavailable longer, the event will be lost. Plan maintenance with this window in mind.
-
Can I use the same URL for multiple webhooks?
Yes. This is common practice. In your code use the
triggerfield for routing and calling the appropriate processing logic.