Using Webhooks: Automate Your Email Workflow
Webhooks let MiN8T push real-time notifications to your systems whenever something happens in your account -- a template is created, an export completes, an ESP connection changes. Instead of polling our API to check for updates, your endpoint receives an HTTP POST the moment an event occurs. This guide covers everything you need to set up, secure, and maintain webhook integrations with MiN8T.
1 Overview
Webhooks are the connective tissue between MiN8T and the rest of your technology stack. They enable workflows that would otherwise require manual intervention or custom polling scripts.
Common use cases:
- Content synchronization -- when a template is updated in MiN8T, automatically push the new version to your CMS, staging environment, or version control system
- Approval workflows -- trigger a Slack notification or Jira ticket when a new email is exported, so reviewers know there is something to approve
- Audit logging -- record every template change and export event in your compliance system for regulatory traceability
- Automation triggers -- use export events to kick off downstream processes in Make.com, Zapier, or n8n, such as scheduling a campaign, updating a dashboard, or notifying stakeholders
- Integration health monitoring -- detect when an ESP connection drops and alert your operations team immediately, rather than discovering the problem when a campaign fails to send
Webhooks are available on all MiN8T plans. There is no limit to the number of webhook subscriptions you can create, and each subscription can listen to one or more event types.
2 Available Events
MiN8T currently supports seven event types across three categories: template lifecycle, export activity, and ESP connection management.
| Event | Trigger | Payload Summary |
|---|---|---|
template.created | A new email template is created (from scratch, duplicated, or imported) | Template ID, name, creator email, creation method |
template.updated | An existing template's content or metadata is modified and saved | Template ID, name, changed fields, editor email, version number |
template.deleted | A template is moved to trash or permanently deleted | Template ID, name, deletion type (soft/hard), deleted by |
export.completed | An email is exported to an ESP or webhook endpoint | Template ID, design name, HTML, JSON, export target, exporter email |
esp.connected | A new ESP integration is authenticated and activated | Integration ID, ESP name, connection method, connected by |
esp.disconnected | An ESP integration is removed or its credentials expire | Integration ID, ESP name, disconnect reason, disconnected by |
esp.data.synced | Data from an ESP is synchronized (lists, segments, templates pulled) | Integration ID, ESP name, sync type, record counts |
Event filtering: When creating a webhook, you select which events it receives. A webhook subscribed only to export.completed will never receive template or ESP events. Subscribe to multiple events on a single webhook, or create separate webhooks for different event types -- whichever fits your architecture.
3 Getting Started
Before creating your first webhook, you need two things:
- A MiN8T account with access to the Developer Tools settings (available to account owners and team members with the "developer" or "owner" role)
- A webhook endpoint URL -- an HTTPS URL that can receive POST requests and respond with a 2xx status code. This can be a custom server, a serverless function (AWS Lambda, Cloudflare Workers, Vercel), or a third-party automation platform (Make.com, Zapier).
HTTPS required. MiN8T only delivers webhooks to HTTPS endpoints. Plain HTTP URLs will be rejected when creating the subscription. This ensures payload data is encrypted in transit.
To access webhook management:
- Log in to your MiN8T account
- Click the gear icon or navigate to Settings
- Select Developer Tools from the sidebar
- Click Webhooks
This page shows all your active webhook subscriptions, their delivery status, and recent delivery logs. From here you can create new webhooks, edit existing ones, view delivery history, and manually trigger test pings.
4 Creating a Webhook
Click Create Webhook on the webhooks management page. You will be asked for four pieces of information:
- Name -- a descriptive label for your reference (e.g., "Slack notifications" or "CMS sync endpoint"). This is not sent in the webhook payload.
- URL -- the HTTPS endpoint that will receive POST requests. MiN8T validates this URL is reachable before saving.
- Events -- select one or more event types from the list. You can change the selected events later without regenerating the signing secret.
- Active -- toggle to enable or disable delivery. Disabled webhooks retain their configuration but do not receive events.
After clicking Create, MiN8T generates a signing secret and displays it once. Copy this secret and store it securely -- you will need it to verify webhook signatures. The secret cannot be retrieved after this screen. If you lose it, you must rotate the secret (which generates a new one and invalidates the old).
Testing with a ping
After creation, click Send Ping to send a test event to your endpoint. The ping payload looks like this:
{
"event": "ping",
"timestamp": "2026-04-03T10:00:00.000Z",
"data": {
"webhookId": "wh_abc123def456",
"message": "Webhook is configured correctly."
}
}
If your endpoint returns a 2xx response, the ping is marked as successful in the delivery log. If it fails, MiN8T shows the HTTP status code and response body to help you debug.
REST API for webhook management
You can also manage webhooks programmatically through the REST API:
# List all webhooks
curl -H "MiN8T-Api-Auth: YOUR_API_KEY" \
https://api.min8t.com/v1/webhooks
# Create a webhook
curl -X POST -H "MiN8T-Api-Auth: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production sync",
"url": "https://api.yourapp.com/webhooks/min8t",
"events": ["template.updated", "export.completed"],
"active": true
}' \
https://api.min8t.com/v1/webhooks
# Delete a webhook
curl -X DELETE -H "MiN8T-Api-Auth: YOUR_API_KEY" \
https://api.min8t.com/v1/webhooks/wh_abc123def456
5 Payload Format
Every webhook delivery is a POST request with a JSON body. The top-level structure is consistent across all event types:
{
"event": "template.updated",
"timestamp": "2026-04-03T14:30:00.000Z",
"data": {
"templateId": "tpl_a1b2c3d4e5f6",
"name": "April Newsletter",
"updatedBy": "designer@company.com",
"version": 12,
"changedFields": ["html", "subject"],
"subject": "Your April roundup is here",
"preheader": "New features, tips, and community highlights"
}
}
The event field tells you which event occurred. The timestamp is always ISO 8601 in UTC. The data object varies by event type -- see the table in section 2 for a summary of what each event includes.
Headers included with every delivery
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | MiN8T-Webhooks/1.0 |
X-MiN8T-Event | Event name (e.g., template.updated) |
X-MiN8T-Delivery | Unique delivery ID (UUID) for deduplication |
X-MiN8T-Signature | HMAC-SHA256 signature of the request body |
Deduplication: Use the X-MiN8T-Delivery header as an idempotency key. If a delivery is retried (see section 7), the same delivery ID is sent. Store processed delivery IDs and skip duplicates to ensure your handler is idempotent.
6 Signature Verification
Every webhook delivery includes an X-MiN8T-Signature header containing an HMAC-SHA256 hash of the raw request body, computed using your webhook's signing secret. You should always verify this signature before processing the payload.
Why verification matters:
- Authenticity -- confirms the request actually came from MiN8T, not a third party who discovered your endpoint URL
- Integrity -- confirms the payload was not modified in transit by a proxy, CDN, or man-in-the-middle
- Non-repudiation -- only MiN8T and your system know the signing secret, so a valid signature proves origin
Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example (use raw body for signature verification)
app.post('/webhooks/min8t', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-min8t-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
console.log(`Received ${event.event} at ${event.timestamp}`);
// Respond quickly, process asynchronously
res.status(200).json({ received: true });
processEventAsync(event);
});
Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhooks/min8t', methods=['POST'])
def handle_webhook():
sig = request.headers.get('X-MiN8T-Signature', '')
if not verify_webhook(request.data, sig, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
print(f"Received {event['event']} at {event['timestamp']}")
# Queue for async processing
tamin8t_sk_queue.enqueue(process_event, event)
return jsonify({'received': True}), 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-MiN8T-Signature")
if !verifyWebhook(body, sig, webhookSecret) {
http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized)
return
}
fmt.Fprintf(w, `{"received":true}`)
go processEvent(body) // async processing
}
Use the raw body. Compute the signature from the raw request bytes, not from a parsed-and-re-serialized JSON object. JSON serialization can reorder keys, change whitespace, or alter number formatting, which would produce a different hash. In Express, use express.raw(). In Flask, use request.data. In Go, use io.ReadAll(r.Body).
7 Retry Policy
If your endpoint does not return a 2xx status code within 10 seconds, MiN8T considers the delivery failed and schedules a retry. The retry schedule uses exponential backoff:
| Attempt | Delay After Failure | Cumulative Wait |
|---|---|---|
| 1st retry | 1 minute | 1 minute |
| 2nd retry | 5 minutes | 6 minutes |
| 3rd retry | 30 minutes | 36 minutes |
| 4th retry | 2 hours | 2 hours 36 minutes |
| 5th retry (final) | 12 hours | 14 hours 36 minutes |
After the 5th retry fails, the delivery is marked as permanently failed in the delivery log. MiN8T does not retry again automatically. You can manually re-send any failed delivery from the webhook management UI.
What counts as success or failure
- Success (no retry): HTTP status 200, 201, 202, 204, or any other 2xx response
- Failure (triggers retry): HTTP status 3xx, 4xx, 5xx, connection timeout, DNS resolution failure, TLS handshake failure, or no response within 10 seconds
Automatic disabling: If a webhook fails on every delivery for 7 consecutive days, MiN8T automatically disables it and sends an email notification to the account owner. Re-enable it from the webhooks management page once your endpoint is back online.
8 Best Practices
Building a reliable webhook consumer is straightforward if you follow these guidelines:
Respond quickly
Return a 2xx response within 5 seconds. Do not process the event synchronously in your request handler. Accept the delivery, enqueue the event for background processing, and respond immediately. This prevents timeouts and retries for events that simply take a while to process.
// Good: respond first, process later
app.post('/webhooks/min8t', (req, res) => {
res.status(200).json({ received: true });
queue.add('process-webhook', req.body);
});
// Bad: process synchronously (risks timeout)
app.post('/webhooks/min8t', async (req, res) => {
await syncToDatabase(req.body); // 3 seconds
await notifySlack(req.body); // 2 seconds
await updateDashboard(req.body); // 4 seconds -- timeout!
res.status(200).json({ received: true });
});
Handle duplicates with idempotency
Due to retries and network conditions, your endpoint may receive the same event more than once. Use the X-MiN8T-Delivery header as an idempotency key:
- Store the delivery ID after processing an event
- Before processing, check if the delivery ID has already been handled
- If it has, return 200 without processing again
Always verify signatures
Never skip signature verification, even in development. It takes a few lines of code (see section 6) and protects you from forged payloads. In development, use a tool like ngrok to expose your local server with HTTPS, and use the real signing secret.
Monitor delivery health
Check the webhook delivery log in Settings → Developer Tools → Webhooks regularly. Look for patterns of failures, which might indicate:
- Consistent 5xx errors -- your endpoint is crashing. Check application logs.
- Consistent timeouts -- your handler is too slow. Implement async processing.
- Intermittent failures -- network issues or rate limiting from upstream services your handler calls.
- 401/403 errors -- your endpoint's authentication may have changed. Verify the signing secret.
Use separate webhooks for different concerns
Rather than routing all events to a single endpoint and parsing them internally, consider creating separate webhooks for different systems. For example: one webhook for your CMS sync (subscribes to template.updated), another for your Slack notifications (subscribes to export.completed), and a third for your audit log (subscribes to all events). This keeps each handler simple and reduces blast radius if one endpoint has issues.
- HTTPS endpoint returning 2xx within 5 seconds
- Signature verification using HMAC-SHA256
- Async processing with a job queue
- Idempotency via delivery ID deduplication
- Signing secret stored in environment variables, not code
- Delivery log monitored for failure patterns
- Separate webhooks for separate concerns
Automate your email workflow
Set up your first webhook in under a minute and connect MiN8T to your stack.
Get started free