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

1
Create a webhook

Use the Manage tab or POST /webhooks API to register your HTTPS endpoint URL and choose which events to subscribe to.

2
Save your secret

Copy the 64-character secret shown once at creation. Store it securely. You will need it to verify signatures.

3
Implement verification

Add HMAC-SHA256 signature verification to your endpoint using the X-Webhook-Signature header.

4
Process events

Parse the JSON payload, extract the event type and data, and handle the event in your application.

5
Return 200 quickly

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.created

A new email template is created

Payload Example
{
  "event": "template.created",
  "timestamp": "2026-02-21T14: 30: 00.000Z",
  "data": {
    "template_id": 1234,
    "name": "Welcome Email",
    "created_by": 42
  }
}
template.updated

An existing template is saved with changes

Payload Example
{
  "event": "template.updated",
  "timestamp": "2026-02-21T14: 35: 00.000Z",
  "data": {
    "template_id": 1234,
    "name": "Welcome Email v2",
    "updated_by": 42
  }
}
template.deleted

A template is permanently deleted

Payload Example
{
  "event": "template.deleted",
  "timestamp": "2026-02-21T14: 40: 00.000Z",
  "data": {
    "template_id": 1234,
    "deleted_by": 42
  }
}
template.duplicated

A template is cloned as a new copy

Payload Example
{
  "event": "template.duplicated",
  "timestamp": "2026-02-21T14: 45: 00.000Z",
  "data": {
    "source_template_id": 1234,
    "new_template_id": 1235,
    "duplicated_by": 42
  }
}

Export

export.started

An email export job begins processing

Payload Example
{
  "event": "export.started",
  "timestamp": "2026-02-21T15: 00: 00.000Z",
  "data": {
    "export_id": "exp_abc123",
    "template_id": 1234,
    "format": "html",
    "initiated_by": 42
  }
}
export.completed

An export finishes successfully

Payload Example
{
  "event": "export.completed",
  "timestamp": "2026-02-21T15: 01: 00.000Z",
  "data": {
    "export_id": "exp_abc123",
    "template_id": 1234,
    "format": "html",
    "size_bytes": 45230
  }
}
export.failed

An export job fails after all retries

Payload Example
{
  "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.connected

An ESP integration is successfully connected

Payload Example
{
  "event": "esp.connected",
  "timestamp": "2026-02-21T16: 00: 00.000Z",
  "data": {
    "esp": "sendgrid",
    "connection_id": "conn_xyz",
    "connected_by": 42
  }
}
esp.disconnected

An ESP integration is manually disconnected

Payload Example
{
  "event": "esp.disconnected",
  "timestamp": "2026-02-21T16: 05: 00.000Z",
  "data": {
    "esp": "sendgrid",
    "connection_id": "conn_xyz",
    "disconnected_by": 42
  }
}
esp.connection_failed

An ESP connection attempt fails (auth error, timeout, etc.)

Payload Example
{
  "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.synced

Mailing lists are synced from an ESP

Payload Example
{
  "event": "esp.list.synced",
  "timestamp": "2026-02-21T17: 00: 00.000Z",
  "data": {
    "esp": "mailchimp",
    "lists_synced": 5,
    "total_contacts": 12400
  }
}
esp.contact.added

A contact is added to an ESP list

Payload Example
{
  "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.removed

A contact is removed from an ESP list

Payload Example
{
  "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.uploaded

An email campaign is uploaded to an ESP for sending

Payload Example
{
  "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

HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
X-Webhook-SignatureHMAC-SHA256 signature of the payload body (64-character hex)a1b2c3d4e5f6...
X-Webhook-EventThe event type that triggered this deliverytemplate.created
X-Webhook-Delivery-IdUnique UUID for this delivery attempt, useful for idempotency and deduplicationf47ac10b-58cc-4372-a567-0e02b2c3d479

Envelope Structure

JSON Envelope
{
  "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.

Security Warning: Always verify signatures before processing webhooks. Use timing-safe comparison functions to prevent timing attacks.

Verification Steps

  1. Extract the X-Webhook-Signature header from the incoming request
  2. Get the raw request body as a UTF-8 string
  3. Compute HMAC-SHA256 of the body using your webhook secret
  4. Compare the computed hash with the header value using a timing-safe comparison
  5. Reject the request with 401 if the signatures don't match

Code Examples

Node.js
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.

AttemptDelayCumulative Time
1Immediate0s
25 seconds~5s
310 seconds~15s
420 seconds~35s
540 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.

POST/webhooks

Create a new webhook subscription

Auth: MiN8T-Api-Auth header

Request Body
{
  "url": "https://example.com/webhooks",
  "events": ["template.created", "export.completed"]
}
Response
{
  "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"
}
Error Codes:400: Invalid URL (must be HTTPS) or invalid events 401: Missing or invalid API key 429: Rate limit exceeded
GET/webhooks

List all webhook subscriptions

Auth: MiN8T-Api-Auth header

Response
{
  "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 }
    }
  ]
}
Error Codes:401: Missing or invalid API key
DELETE/webhooks/:webhook_id

Delete a webhook subscription

Auth: MiN8T-Api-Auth header

Response
{
  "deleted": true,
  "webhook_id": "wh_1708512600_a3f2b1"
}
Error Codes:401: Missing or invalid API key 404: Webhook not found or not owned by user
PATCH/webhooks/:webhook_id/status

Toggle a webhook between active and inactive

Auth: MiN8T-Api-Auth header

Request Body
{
  "status": "inactive"
}
Response
{
  "webhook_id": "wh_1708512600_a3f2b1",
  "status": "inactive"
}
Error Codes:400: Invalid status (must be "active" or "inactive") 401: Missing or invalid API key 404: Webhook not found
POST/webhooks/incoming/:esp

Receive incoming webhooks from ESP platforms (e.g., SendGrid, Mailchimp)

Auth: X-Webhook-Signature header (HMAC)

Request Body
{
  "event_type": "delivered",
  "email": "user@example.com",
  "timestamp": "2026-02-21T15: 00: 00.000Z",
  "payload": { "message_id": "msg_abc123" },
  "signature": "a1b2c3..."
}
Response
{
  "received": true
}
Error Codes: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.