Webhooks Overview
Get real-time notifications about email events via webhooks
Webhooks deliver real-time HTTP notifications to your server when email events occur, deliveries, bounces, complaints, and more. Instead of polling for status changes, MailLand pushes events to your endpoint as they happen.
How Webhooks Work
Configure a webhook
Register your endpoint URL and select which events you want to receive.
Mail.Land sends events
When an event occurs (e.g., an email is delivered), MailLand sends a signed HTTP POST to your endpoint with the event payload.
Your server responds
Return a 2xx status code to acknowledge receipt. Non-2xx responses trigger automatic retries.
Create a Webhook
POST https://api.mail.land/v1/webhookscurl -X POST https://api.mail.land/v1/webhooks \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Production Events",
"webhookUrl": "https://your-app.com/webhooks/mailland",
"products": ["transactional", "marketing"],
"events": ["message.delivered", "message.bounced", "message.deferred", "message.complained"],
"includeFutureEvents": true,
"scope": "all_domains"
}'Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Human-readable name for this webhook |
webhookUrl | string | Yes | HTTPS endpoint URL to receive events |
products | array | No | Product filter: transactional, marketing, inbound, system |
events | array | No | Specific events to subscribe to (see Events) |
includeFutureEvents | boolean | No | Receive new events added to subscribed products (default: true) |
scope | string | No | all_domains (default) or selected_domains |
domainIds | array | No | Domain IDs to filter by (required if scope is selected_domains) |
Response
Status: 201 Created
{
"webhook": {
"id": "k3m9x2p7qw4rtz",
"name": "Production Events",
"webhookUrl": "https://your-app.com/webhooks/mailland",
"products": ["transactional", "marketing"],
"events": ["message.delivered", "message.bounced", "message.deferred", "message.complained"],
"includeFutureEvents": true,
"scope": "all_domains",
"domainIds": [],
"isActive": true,
"createdAt": "2026-02-17T10:00:00Z",
"successCount": 0,
"failureCount": 0,
"consecutiveFailures": 0,
"pauseThreshold": 10
},
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0..."
}The secret is only shown once when the webhook is created. Store it securely: you'll need it to verify webhook signatures. If you lose it, you can regenerate it, which invalidates the old secret.
Webhook URLs must use HTTPS. MailLand validates URLs against SSRF attacks: private network addresses (127.0.0.1, 10.x, 192.168.x, etc.) are rejected. Delivery is to HTTPS on port 443 only; a URL on another port, or one that points at MailLand's own servers or host names, is refused with 400 WEBHOOK_DESTINATION_NOT_ALLOWED. A different TLS port needs an explicit approval from MailLand support.
List Webhooks
GET https://api.mail.land/v1/webhookscurl -X GET https://api.mail.land/v1/webhooks \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."[
{
"id": "k3m9x2p7qw4rtz",
"name": "Production Events",
"webhookUrl": "https://your-app.com/webhooks/mailland",
"products": ["transactional", "marketing"],
"events": ["message.delivered", "message.bounced"],
"isActive": true,
"lastTriggeredAt": "2026-02-17T12:30:00Z",
"lastSuccessAt": "2026-02-17T12:30:00Z",
"successCount": 1542,
"failureCount": 3,
"consecutiveFailures": 0,
"createdAt": "2026-02-10T08:00:00Z"
}
]Returns up to 100 webhooks ordered by creation date (newest first).
Get Webhook Detail
GET https://api.mail.land/v1/webhooks/{id}curl -X GET https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Returns the full webhook configuration plus the 50 most recent delivery logs. See Retry Logic for delivery log details.
Update a Webhook
PUT https://api.mail.land/v1/webhooks/{id}curl -X PUT https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "All Events",
"events": ["message.delivered", "message.bounced", "message.deferred", "message.complained"],
"isActive": true
}'All fields are optional: only include the fields you want to change.
Test a Webhook
Send a test event to verify your endpoint is working:
POST https://api.mail.land/v1/webhooks/{id}/testcurl -X POST https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz/test \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"success": true,
"statusCode": 200,
"statusText": "OK",
"durationMs": 145,
"responseBody": "{\"received\": true}",
"headersSent": {
"X-MLND-Signature": "t=1708171200,v1=abc123...",
"X-MLND-Event-Id": "evt_test_abc123",
"X-MLND-Event-Version": "1",
"X-MLND-Event": "webhook.test",
"X-MLND-Webhook-Id": "k3m9x2p7qw4rtz",
"X-MLND-Attempt": "1"
},
"wouldReceiveRealEvents": true,
"liveEvents": ["message.delivered", "message.bounced"],
"dormantEvents": {},
"subscriptionWarning": null
}The test send uses the same signing and the same delivery transport as a real event, so a success here proves your URL, your TLS and your signature verification.
It does not prove your subscription: a test is sent on demand, not routed by your event selection. The last four fields answer that separately:
| Field | Meaning |
|---|---|
wouldReceiveRealEvents | Whether any real event can reach this endpoint at all |
liveEvents | Subscribed events that pass the same filter the dispatcher applies |
dormantEvents | Subscribed events that will never arrive, each with the reason |
subscriptionWarning | Set when the endpoint is reachable but permanently idle |
A "success": true alongside "wouldReceiveRealEvents": false is the important case: the endpoint
works and is subscribed to nothing that can be delivered.
If the test fails, the errorType field indicates the reason: ssrf_blocked (the URL, or a redirect
from it, pointed somewhere the platform refuses to send to) or network_error (timeout, TLS failure,
connection refused, errorMessage carries the detail).
Regenerate Secret
If you lose or need to rotate your webhook secret:
POST https://api.mail.land/v1/webhooks/{id}/regenerate-secretcurl -X POST https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz/regenerate-secret \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"secretKey": "whsec_x9y8z7w6v5u4..."
}Regenerating the secret immediately invalidates the old one. Update your server's verification code before the next webhook delivery, or you'll reject valid events.
Domain Scope
By default, webhooks receive events for all domains in your workspace. To limit a webhook to specific domains:
{
"webhookUrl": "https://your-app.com/webhooks/mailland",
"scope": "selected_domains",
"domainIds": ["d4n8q1zt5wm3xc", "b7r2mv9k0jy4pw"]
}Only events related to those domains will trigger the webhook.
Auto-Pause on Failures
MailLand automatically pauses webhooks after 10 consecutive failed delivery attempts. This prevents wasting resources on endpoints that are down. The counter is of attempts, not of events, one event that exhausts its retry ladder contributes several failures, and any successful delivery resets it to zero.
When a webhook is paused:
isActivebecomesfalse- The
pausedAttimestamp is set - The
pausedReasondescribes why: the failure count and the last error - An in-app notification is created
Resume a Paused Webhook
After fixing the issue, resume delivery:
POST https://api.mail.land/v1/webhooks/{id}/resumecurl -X POST https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz/resume \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."This clears the pause state, resets the consecutive failure counter, and reactivates the webhook (isActive becomes true again), so future events are delivered to it normally.
Delete a Webhook
DELETE https://api.mail.land/v1/webhooks/{id}curl -X DELETE https://api.mail.land/v1/webhooks/k3m9x2p7qw4rtz \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Returns 204 No Content. If the webhook is used by inbound email routes, those routes will be updated or disabled automatically.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | INVALID_REQUEST | Invalid URL, events, or scope configuration |
403 | PLAN_LIMIT_REACHED | Maximum webhooks for your plan reached |
404 | (empty body) | Webhook ID does not exist: returned by get, update, delete, regenerate-secret and resume |
404 | RESOURCE_NOT_FOUND | Webhook ID does not exist, returned by /test only |
Next Steps
- Events: complete reference of all event types and payloads
- Signature Verification: verify webhook authenticity
- Retry Logic: understand retry behavior and failure handling