Webhooks.

The platform POSTs a JSON array of events to a URL you choose — opens, clicks, unsubscribes, SMTP bounces and system notifications — so you can update your own records as things happen instead of polling for them. Unsubscribes are the one not to skip: acting on them in your database is what keeps your suppression honest.

Delivery

How a webhook is delivered.

  • Up to 100 records per POST. Events are batched, so a large campaign doesn't flood your endpoint one request at a time.
  • Your endpoint must return HTTP 200. Any other status counts as a failure.
  • Failed deliveries are retried five times, at 5 minutes, 1 hour, 3 hours, 12 hours and 24 hours after the first attempt.
  • 100 consecutive failures disables the webhook. It fails quietly from your side, so alert on it.
  • timestamp is a UNIX timestamp in CST (Central Standard Time), not UTC. This is the single most common integration mistake.
  • listmetadata is optional: a JSON object with every column from your list for that email record. Turn on Include Full List Column Data when you configure the webhook.
  • Webhooks are created and removed in the platform, or by API: addwebhook · getwebhooks · deletewebhook.
Security

Verify the signature before you trust the payload.

Every POST carries an APISIGNATURE header: an HMAC-SHA512 of the raw request body, using your Master API Key as the shared secret. Recompute it over the exact bytes you received and compare with a constant-time check. Anything that doesn't match isn't from us.

PHP
$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_APISIGNATURE'] ?? '';
$calc = hash_hmac('sha512', $body, MASTER_API_KEY);
if (!hash_equals($calc, $sig)) {
    http_response_code(401);
    exit;
}
$events = json_decode($body, true);
// store the batch, then update your records
http_response_code(200);
Python (Flask)
import hmac, hashlib

body = request.get_data()                       # raw bytes, not parsed
sig  = request.headers.get("APISIGNATURE", "")
calc = hmac.new(MASTER_API_KEY.encode(), body, hashlib.sha512).hexdigest()
if not hmac.compare_digest(calc, sig):
    abort(401)
events = request.get_json()
# store the batch, then update your records
return "", 200
Event types

Eight events you can subscribe to.

01

open

An email was opened.

02

click

A link in an email was clicked.

03

unsubscribe

A recipient unsubscribed.

04

smtpopen

An email sent through the SMTP API was opened.

05

smtpclick

A link in an SMTP API email was clicked.

06

smtphardbounce

An SMTP API email hard-bounced.

07

smtpsoftbounce

An SMTP API email soft-bounced.

08

notification

System events: list completion (email and SMS), merge completion, SMS 10DLC events, DBL alerts, campaign errors, and list verification job completion.

Payloads

What arrives at your URL.

Always a JSON array, even for a single event. Field names are lowercase; action repeats the event type on every record.

open — an email was opened
[
  {
    "requestid": "13",
    "timestamp": "1670694746",
    "email": "sales@test.com",
    "ipaddress": "127.0.0.1",
    "useragent": "Mozilla/4.0 (compatible; ms-office; MSOffice 16)",
    "action": "open"
  },
  {
    "requestid": "14",
    "timestamp": "1670696677",
    "email": "sales@test.com",
    "ipaddress": "127.0.0.1",
    "useragent": "Mozilla/4.0 (compatible; ms-office; MSOffice 16)",
    "action": "open"
  }
]
click — a link was clicked
[
  {
    "requestid": "5",
    "timestamp": "1670646495",
    "email": "sales@test.com",
    "ipaddress": "127.0.0.1",
    "link": "https://google.com",
    "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
    "action": "click"
  }
]
unsubscribe — a recipient unsubscribed
[
  {
    "requestid": "10",
    "timestamp": "1670657076",
    "email": "test@mydomain.com",
    "ipaddress": "127.0.0.1",
    "useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
    "action": "unsubscribe"
  }
]
smtpsoftbounce — SMTP API soft bounce
[
  {
    "requestid": "577880",
    "timestamp": "1753408476",
    "email": "random2@test.com",
    "metadata": "",
    "emailstatus": "Timed Out",
    "debugreason": "Connect timed out.",
    "action": "smtpsoftbounce"
  }
]
smtphardbounce — SMTP API hard bounce
[
  {
    "requestid": "577881",
    "timestamp": "1753408476",
    "email": "badrandom2@test2.com",
    "metadata": "",
    "emailstatus": "Address does not exist",
    "debugreason": "Requested action not taken: mailbox unavailable or not local.",
    "action": "smtphardbounce"
  }
]
Implementation

Three habits that keep webhooks boring.

  • Respond fast, process later. Return 200 as soon as you've stored the batch; do the database work after. Slow endpoints look like failures and trigger retries.
  • Always validate APISIGNATURE. It's the only thing standing between your endpoint and a spoofed request.
  • Log every batch you receive. When something looks off, the raw log is what tells you whether the events never arrived or arrived and were mishandled.

Need a call that isn't here?

Custom integration work is quoted on scope, and support is a person on weekdays, 9am to 5pm CST.