Overview
Webhooks let your application receive real-time HTTP notifications when events occur in MiN8T, such as templates being created, exports completing, or ESP connections changing.
Instead of polling the API for changes, you register an HTTPS endpoint URL and choose which events to subscribe to. When an event fires, MiN8T sends a POST request to your URL with a signed JSON payload.
Quick Setup
Use the Manage tab or POST /webhooks API to register your HTTPS endpoint URL and choose which events to subscribe to.
Copy the 64-character secret shown once at creation. Store it securely. You will need it to verify signatures.
Add HMAC-SHA256 signature verification to your endpoint using the X-Webhook-Signature header.
Parse the JSON payload, extract the event type and data, and handle the event in your application.
Acknowledge receipt with a 200 response within 5 seconds. Process heavy work asynchronously.
Events Reference
MiN8T supports 14 webhook event types across 4 categories.
Template
template.createdA new email template is created
{
"event": "template.created",
"timestamp": "2026-02-21T14: 30: 00.000Z",
"data": {
"template_id": 1234,
"name": "Welcome Email",
"created_by": 42
}
}template.updatedAn existing template is saved with changes
{
"event": "template.updated",
"timestamp": "2026-02-21T14: 35: 00.000Z",
"data": {
"template_id": 1234,
"name": "Welcome Email v2",
"updated_by": 42
}
}template.deletedA template is permanently deleted
{
"event": "template.deleted",
"timestamp": "2026-02-21T14: 40: 00.000Z",
"data": {
"template_id": 1234,
"deleted_by": 42
}
}template.duplicatedA template is cloned as a new copy
{
"event": "template.duplicated",
"timestamp": "2026-02-21T14: 45: 00.000Z",
"data": {
"source_template_id": 1234,
"new_template_id": 1235,
"duplicated_by": 42
}
}Export
export.startedAn email export job begins processing
{
"event": "export.started",
"timestamp": "2026-02-21T15: 00: 00.000Z",
"data": {
"export_id": "exp_abc123",
"template_id": 1234,
"format": "html",
"initiated_by": 42
}
}export.completedAn export finishes successfully
{
"event": "export.completed",
"timestamp": "2026-02-21T15: 01: 00.000Z",
"data": {
"export_id": "exp_abc123",
"template_id": 1234,
"format": "html",
"size_bytes": 45230
}
}export.failedAn export job fails after all retries
{
"event": "export.failed",
"timestamp": "2026-02-21T15: 02: 00.000Z",
"data": {
"export_id": "exp_abc123",
"template_id": 1234,
"error": "Template rendering timeout"
}
}ESP Connection
esp.connectedAn ESP integration is successfully connected
{
"event": "esp.connected",
"timestamp": "2026-02-21T16: 00: 00.000Z",
"data": {
"esp": "sendgrid",
"connection_id": "conn_xyz",
"connected_by": 42
}
}esp.disconnectedAn ESP integration is manually disconnected
{
"event": "esp.disconnected",
"timestamp": "2026-02-21T16: 05: 00.000Z",
"data": {
"esp": "sendgrid",
"connection_id": "conn_xyz",
"disconnected_by": 42
}
}esp.connection_failedAn ESP connection attempt fails (auth error, timeout, etc.)
{
"event": "esp.connection_failed",
"timestamp": "2026-02-21T16: 10: 00.000Z",
"data": {
"esp": "mailchimp",
"error": "Invalid API key",
"attempted_by": 42
}
}ESP Data
esp.list.syncedMailing lists are synced from an ESP
{
"event": "esp.list.synced",
"timestamp": "2026-02-21T17: 00: 00.000Z",
"data": {
"esp": "mailchimp",
"lists_synced": 5,
"total_contacts": 12400
}
}esp.contact.addedA contact is added to an ESP list
{
"event": "esp.contact.added",
"timestamp": "2026-02-21T17: 05: 00.000Z",
"data": {
"esp": "sendgrid",
"list_id": "list_abc",
"contact_email": "user@example.com"
}
}esp.contact.removedA contact is removed from an ESP list
{
"event": "esp.contact.removed",
"timestamp": "2026-02-21T17: 10: 00.000Z",
"data": {
"esp": "sendgrid",
"list_id": "list_abc",
"contact_email": "user@example.com"
}
}esp.campaign.uploadedAn email campaign is uploaded to an ESP for sending
{
"event": "esp.campaign.uploaded",
"timestamp": "2026-02-21T17: 15: 00.000Z",
"data": {
"esp": "mailchimp",
"campaign_id": "camp_xyz",
"template_id": 1234,
"subject": "February Newsletter"
}
}Payload Format
Every webhook delivery uses the same envelope structure and HTTP headers.
HTTP Headers
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
X-Webhook-Signature | HMAC-SHA256 signature of the payload body (64-character hex) | a1b2c3d4e5f6... |
X-Webhook-Event | The event type that triggered this delivery | template.created |
X-Webhook-Delivery-Id | Unique UUID for this delivery attempt, useful for idempotency and deduplication | f47ac10b-58cc-4372-a567-0e02b2c3d479 |
Envelope Structure
{
"event": "template.created",
"timestamp": "2026-02-21T14: 30: 00.000Z",
"data": {
// Event-specific fields
}
}Signature Verification
Every webhook delivery includes an X-Webhook-Signature header, an HMAC-SHA256 hash of the raw request body signed with your webhook secret.
Verification Steps
- Extract the
X-Webhook-Signatureheader from the incoming request - Get the raw request body as a UTF-8 string
- Compute HMAC-SHA256 of the body using your webhook secret
- Compare the computed hash with the header value using a timing-safe comparison
- Reject the request with 401 if the signatures don't match
Code Examples
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// Express middleware example
app.post('/my-webhook-endpoint', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook event
const { event, data } = req.body;
console.log(`Received: ${event}`, data);
res.status(200).json({ received: true });
});Retry Policy
If your endpoint doesn't return a 2xx response within 5 seconds, MiN8T retries with exponential backoff.
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 5 seconds | ~5s |
| 3 | 10 seconds | ~15s |
| 4 | 20 seconds | ~35s |
| 5 | 40 seconds | ~75s |
After 5 failed attempts (~75 seconds total), the delivery is marked as permanently failed. Failed deliveries are logged and visible in the webhook stats.
Idempotency
Due to retries, your endpoint may receive the same event more than once. Use the event type and timestamp fields to deduplicate. Design your handlers to be idempotent. Processing the same event twice should produce the same result.
API Reference
Manage webhooks programmatically via the Integration Service API.
/webhooksCreate a new webhook subscription
Auth: MiN8T-Api-Auth header
{
"url": "https://example.com/webhooks",
"events": ["template.created", "export.completed"]
}{
"webhook_id": "wh_1708512600_a3f2b1",
"url": "https://example.com/webhooks",
"events": ["template.created", "export.completed"],
"secret": "a1b2c3d4e5f6... (64-char hex, shown only once)",
"created_at": "2026-02-21T14: 30: 00.000Z"
}400: Invalid URL (must be HTTPS) or invalid events 401: Missing or invalid API key 429: Rate limit exceeded /webhooksList all webhook subscriptions
Auth: MiN8T-Api-Auth header
{
"webhooks": [
{
"webhook_id": "wh_1708512600_a3f2b1",
"url": "https://example.com/webhooks",
"events": ["template.created", "export.completed"],
"status": "active",
"created_at": "2026-02-21T14: 30: 00.000Z",
"last_triggered_at": "2026-02-21T15: 00: 00.000Z",
"stats": { "success": 42, "failures": 1 }
}
]
}401: Missing or invalid API key /webhooks/:webhook_idDelete a webhook subscription
Auth: MiN8T-Api-Auth header
{
"deleted": true,
"webhook_id": "wh_1708512600_a3f2b1"
}401: Missing or invalid API key 404: Webhook not found or not owned by user /webhooks/:webhook_id/statusToggle a webhook between active and inactive
Auth: MiN8T-Api-Auth header
{
"status": "inactive"
}{
"webhook_id": "wh_1708512600_a3f2b1",
"status": "inactive"
}400: Invalid status (must be "active" or "inactive") 401: Missing or invalid API key 404: Webhook not found /webhooks/incoming/:espReceive incoming webhooks from ESP platforms (e.g., SendGrid, Mailchimp)
Auth: X-Webhook-Signature header (HMAC)
{
"event_type": "delivered",
"email": "user@example.com",
"timestamp": "2026-02-21T15: 00: 00.000Z",
"payload": { "message_id": "msg_abc123" },
"signature": "a1b2c3..."
}{
"received": true
}400: Missing required fields or invalid payload 401: Invalid webhook signature 429: Rate limit exceeded Best Practices
Follow these guidelines for reliable and secure webhook handling.
Always verify signatures
Use HMAC-SHA256 with timing-safe comparison on every incoming webhook. Never process unverified payloads.
Use HTTPS endpoints only
MiN8T requires HTTPS for all webhook URLs. This ensures payloads are encrypted in transit.
Respond quickly (< 5s)
Return a 200 status immediately and process the event asynchronously. Long response times trigger retries.
Handle duplicates (idempotency)
Due to retries, you may receive the same event more than once. Use the X-Webhook-Delivery-Id header to deduplicate. Each delivery attempt includes a unique UUID for reliable idempotency.
Store your secret securely
The webhook secret is only shown once at creation. Store it in environment variables or a secrets manager, never in source code.
Log all deliveries
Keep a log of received webhooks for debugging. Include the event type, timestamp, and processing result.
Monitor failure rates
Use the webhook stats endpoint to track success/failure counts. High failure rates may indicate endpoint issues.
Return meaningful status codes
Return 200 for success, 401 for signature failures, and 500 for processing errors. This helps MiN8T distinguish between retryable and permanent failures.