Reliable email and SMS webhook processing
Documentation navigation
Verify incoming events, queue work durably, prevent duplicate actions and recover failed webhook deliveries.
Use webhooks to start processing when an email, SMS, attachment or AI result arrives. MailSlurp delivers events at least once, so a successful receiver needs durable processing and duplicate handling in addition to a reachable URL.
Receive, verify and queue
- Parse the supported event type and retain its
messageIdand source resource IDs. - Verify
x-signaturewithx-message-idusing MailSlurp's signature verification endpoint. - Atomically insert the event into a durable queue or inbox table with a unique constraint on
messageId. - Return a 2xx response after the insert succeeds. An already-recorded duplicate can also return 2xx.
- Let a worker fetch the source message or result, process it and record completion.
/webhooks/verify
Verify a webhook payload signature
Verify a webhook payload using the messageId and signature. This allows you to be sure that MailSlurp sent the payload and not another server.
Request, parameters, and responses
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
messageId | string | Yes | |
signature | string | Yes |
{
"messageId": "00000000-0000-4000-8000-000000000000",
"signature": "value"
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | VerifyWebhookSignatureResults | OK |
HTTP examples
HTTP
POST /webhooks/verify HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{
"messageId": "value",
"signature": "value"
}
cURL
Requires cURL 7.76+ and a POSIX shell. Tested with cURL 8.12.1.
(
set -eu
response_file=$(mktemp)
request_body=$(mktemp)
trap 'rm -f "$response_file" "$request_body"' EXIT
{
printf '%b' '{\0012 "messageId": "value",\0012 "signature": "value"\0012}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/webhooks/verify' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--output "$response_file" \
--write-out '%{http_code}' \
--data-binary "@$request_body")
case "$status" in
2??) mv "$response_file" 'response.bin' ;;
*) printf 'HTTP %s\n' "$status" >&2; cat "$response_file" >&2; exit 1 ;;
esac
)
JavaScript (HTTP)
Requires Node.js 22+. Tested with Node.js 22.14.0.
import { writeFile } from "node:fs/promises";
const body = Buffer.concat([
Buffer.from("{\n \"messageId\": \"value\",\n \"signature\": \"value\"\n}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/webhooks/verify", {
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
},
body,
redirect: "manual",
signal: AbortSignal.timeout(60000),
});
const responseBytes = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseBytes.toString("utf8")}`);
}
await writeFile("response.bin", responseBytes);
Python (HTTP)
Requires Python 3.9+. Tested with Python 3.9.21.
import http.client
from pathlib import Path
from urllib.parse import urlsplit
url = urlsplit("https://api.mailslurp.com/webhooks/verify")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{\n \"messageId\": \"value\",\n \"signature\": \"value\"\n}".encode("utf-8")
])
connection_class = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
connection = connection_class(url.hostname, url.port, timeout=60)
try:
path = (url.path or "/") + ("?" + url.query if url.query else "")
connection.request("POST", path, body=body, headers=headers)
response = connection.getresponse()
response_bytes = response.read()
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response_bytes.decode('utf-8', errors='replace')}")
Path("response.bin").write_bytes(response_bytes)
finally:
connection.close()
Do not acknowledge before durable storage: a crash after acknowledgement can lose work. Do not use an in-memory set as the only duplicate store because it disappears on restart and is not shared between workers.
Make downstream actions repeatable
A worker can crash after writing to a CRM or sending a reply but before recording success. Use a destination idempotency key or transaction keyed to the logical action, not just a local boolean. Keep the source event, extracted result and destination record associated for recovery.
For AI extraction results, validate the value before writing it. For agent replies, use claims, freshness guards and the completion endpoint's idempotency key. Apply the same principle to SMS processing.
Inspect failures and redrive
MailSlurp retries unsuccessful webhook deliveries with backoff for up to 24 hours. Inspect the delivery result and HTTP response to distinguish an unreachable endpoint, authentication failure and application error. Fix the receiver, then redrive the affected result.
/webhooks/{webhookId}/results
Get a webhook results for a webhook
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
webhookId | string:uuid | Yes | ID of webhook to get results for |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | integer:int32 | No | Optional page index in list pagination |
size | integer:int32 | No | Optional page size in list pagination |
sort | enum: ASC | DESC | No | Optional createdAt sort direction ASC or DESCValues: ASC, DESC |
searchFilter | string | No | Optional search filter |
since | string:date-time | No | Filter by created at after the given timestamp |
before | string:date-time | No | Filter by created at before the given timestamp |
unseenOnly | boolean | No | Filter for unseen exceptions only |
resultType | enum: BAD_RESPONSE | EXCEPTION | EXHAUSTED | SUCCESS | REDRIVEN | No | Filter by result typeValues: BAD_RESPONSE, EXCEPTION, EXHAUSTED, SUCCESS, REDRIVEN |
eventName | enum: EMAIL_RECEIVED | NEW_AI_TRANSFORM_RESULT | NEW_EMAIL | NEW_CONTACT | NEW_ATTACHMENT | EMAIL_OPENED | EMAIL_READ | DELIVERY_STATUS | BOUNCE | BOUNCE_RECIPIENT | NEW_SMS | NEW_GUEST_USER | No | Filter by event nameValues: EMAIL_RECEIVED, NEW_AI_TRANSFORM_RESULT, NEW_EMAIL, NEW_CONTACT, NEW_ATTACHMENT, EMAIL_OPENED, EMAIL_READ, DELIVERY_STATUS, BOUNCE, BOUNCE_RECIPIENT, NEW_SMS, NEW_GUEST_USER |
minStatusCode | integer:int32 | No | Minimum response status |
maxStatusCode | integer:int32 | No | Maximum response status |
inboxId | string:uuid | No | Inbox ID |
smsId | string:uuid | No | Sms ID |
attachmentId | string:uuid | No | Attachment ID |
emailId | string:uuid | No | Email ID |
phoneId | string:uuid | No | Phone ID |
aiTransformerId | string:uuid | No | AI Transformer ID |
Responses
| Status | Schema | Description |
|---|---|---|
200 | PageWebhookResult | OK |
HTTP examples
HTTP
GET /webhooks/00000000-0000-4000-8000-000000000000/results HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: */*
cURL
Requires cURL 7.76+ and a POSIX shell. Tested with cURL 8.12.1.
(
set -eu
response_file=$(mktemp)
trap 'rm -f "$response_file"' EXIT
status=$(curl --silent --show-error \
--request GET \
--url 'https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/results' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Accept: */*' \
--output "$response_file" \
--write-out '%{http_code}')
case "$status" in
2??) mv "$response_file" 'response.bin' ;;
*) printf 'HTTP %s\n' "$status" >&2; cat "$response_file" >&2; exit 1 ;;
esac
)
JavaScript (HTTP)
Requires Node.js 22+. Tested with Node.js 22.14.0.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/results", {
method: "GET",
headers: {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
},
redirect: "manual",
signal: AbortSignal.timeout(60000),
});
const responseBytes = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseBytes.toString("utf8")}`);
}
await writeFile("response.bin", responseBytes);
Python (HTTP)
Requires Python 3.9+. Tested with Python 3.9.21.
import http.client
from pathlib import Path
from urllib.parse import urlsplit
url = urlsplit("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/results")
headers = {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
}
connection_class = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
connection = connection_class(url.hostname, url.port, timeout=60)
try:
path = (url.path or "/") + ("?" + url.query if url.query else "")
connection.request("GET", path, headers=headers)
response = connection.getresponse()
response_bytes = response.read()
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response_bytes.decode('utf-8', errors='replace')}")
Path("response.bin").write_bytes(response_bytes)
finally:
connection.close()
/webhooks/results/{webhookResultId}/redrive
Get a webhook result and try to resend the original webhook payload
Allows you to resend a webhook payload that was already sent. Webhooks that fail are retried automatically for 24 hours and then put in a dead letter queue. You can retry results manually using this method.
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
webhookResultId | string:uuid | Yes | Webhook Result ID |
Responses
| Status | Schema | Description |
|---|---|---|
200 | WebhookRedriveResult | OK |
HTTP examples
HTTP
POST /webhooks/results/00000000-0000-4000-8000-000000000000/redrive HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: */*
cURL
Requires cURL 7.76+ and a POSIX shell. Tested with cURL 8.12.1.
(
set -eu
response_file=$(mktemp)
trap 'rm -f "$response_file"' EXIT
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/webhooks/results/00000000-0000-4000-8000-000000000000/redrive' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Accept: */*' \
--output "$response_file" \
--write-out '%{http_code}')
case "$status" in
2??) mv "$response_file" 'response.bin' ;;
*) printf 'HTTP %s\n' "$status" >&2; cat "$response_file" >&2; exit 1 ;;
esac
)
JavaScript (HTTP)
Requires Node.js 22+. Tested with Node.js 22.14.0.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.mailslurp.com/webhooks/results/00000000-0000-4000-8000-000000000000/redrive", {
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
},
redirect: "manual",
signal: AbortSignal.timeout(60000),
});
const responseBytes = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseBytes.toString("utf8")}`);
}
await writeFile("response.bin", responseBytes);
Python (HTTP)
Requires Python 3.9+. Tested with Python 3.9.21.
import http.client
from pathlib import Path
from urllib.parse import urlsplit
url = urlsplit("https://api.mailslurp.com/webhooks/results/00000000-0000-4000-8000-000000000000/redrive")
headers = {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
}
connection_class = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
connection = connection_class(url.hostname, url.port, timeout=60)
try:
path = (url.path or "/") + ("?" + url.query if url.query else "")
connection.request("POST", path, headers=headers)
response = connection.getresponse()
response_bytes = response.read()
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response_bytes.decode('utf-8', errors='replace')}")
Path("response.bin").write_bytes(response_bytes)
finally:
connection.close()
Redrive repeats delivery of the original payload, so the receiver must still deduplicate it. Redrive does not repair a downstream worker failure after your endpoint already returned 2xx; recover those jobs through your own queue.
Test before enabling the workflow
Send the API's test payload and confirm signature handling, queue insertion and event routing. Exercise duplicate delivery, receiver failure, worker restart and a downstream timeout. Check that only one business action occurs when an event is replayed.
/webhooks/{webhookId}/test
Send webhook test data
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
webhookId | string:uuid | Yes |
Responses
| Status | Schema | Description |
|---|---|---|
201 | WebhookTestResult | Created |
HTTP examples
HTTP
POST /webhooks/00000000-0000-4000-8000-000000000000/test HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: */*
cURL
Requires cURL 7.76+ and a POSIX shell. Tested with cURL 8.12.1.
(
set -eu
response_file=$(mktemp)
trap 'rm -f "$response_file"' EXIT
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/test' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Accept: */*' \
--output "$response_file" \
--write-out '%{http_code}')
case "$status" in
2??) mv "$response_file" 'response.bin' ;;
*) printf 'HTTP %s\n' "$status" >&2; cat "$response_file" >&2; exit 1 ;;
esac
)
JavaScript (HTTP)
Requires Node.js 22+. Tested with Node.js 22.14.0.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/test", {
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
},
redirect: "manual",
signal: AbortSignal.timeout(60000),
});
const responseBytes = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseBytes.toString("utf8")}`);
}
await writeFile("response.bin", responseBytes);
Python (HTTP)
Requires Python 3.9+. Tested with Python 3.9.21.
import http.client
from pathlib import Path
from urllib.parse import urlsplit
url = urlsplit("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/test")
headers = {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
}
connection_class = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
connection = connection_class(url.hostname, url.port, timeout=60)
try:
path = (url.path or "/") + ("?" + url.query if url.query else "")
connection.request("POST", path, headers=headers)
response = connection.getresponse()
response_bytes = response.read()
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response_bytes.decode('utf-8', errors='replace')}")
Path("response.bin").write_bytes(response_bytes)
finally:
connection.close()
/webhooks/{webhookId}/wait
Wait for webhook results for a webhook
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
webhookId | string:uuid | Yes | ID of webhook to get results for |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
expectedCount | integer:int32 | Yes | Expected result count |
timeout | integer:int32 | Yes | Max time to wait in milliseconds |
Responses
| Status | Schema | Description |
|---|---|---|
200 | WebhookResultDto[] | OK |
HTTP examples
HTTP
GET /webhooks/00000000-0000-4000-8000-000000000000/wait?expectedCount=1&timeout=1000 HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Accept: */*
cURL
Requires cURL 7.76+ and a POSIX shell. Tested with cURL 8.12.1.
(
set -eu
response_file=$(mktemp)
trap 'rm -f "$response_file"' EXIT
status=$(curl --silent --show-error \
--request GET \
--url 'https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/wait?expectedCount=1&timeout=1000' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Accept: */*' \
--output "$response_file" \
--write-out '%{http_code}')
case "$status" in
2??) mv "$response_file" 'response.bin' ;;
*) printf 'HTTP %s\n' "$status" >&2; cat "$response_file" >&2; exit 1 ;;
esac
)
JavaScript (HTTP)
Requires Node.js 22+. Tested with Node.js 22.14.0.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/wait?expectedCount=1&timeout=1000", {
method: "GET",
headers: {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
},
redirect: "manual",
signal: AbortSignal.timeout(60000),
});
const responseBytes = Buffer.from(await response.arrayBuffer());
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${responseBytes.toString("utf8")}`);
}
await writeFile("response.bin", responseBytes);
Python (HTTP)
Requires Python 3.9+. Tested with Python 3.9.21.
import http.client
from pathlib import Path
from urllib.parse import urlsplit
url = urlsplit("https://api.mailslurp.com/webhooks/00000000-0000-4000-8000-000000000000/wait?expectedCount=1&timeout=1000")
headers = {
"x-api-key": "YOUR_API_KEY",
"Accept": "*/*"
}
connection_class = http.client.HTTPSConnection if url.scheme == "https" else http.client.HTTPConnection
connection = connection_class(url.hostname, url.port, timeout=60)
try:
path = (url.path or "/") + ("?" + url.query if url.query else "")
connection.request("GET", path, headers=headers)
response = connection.getresponse()
response_bytes = response.read()
if not 200 <= response.status < 300:
raise RuntimeError(f"HTTP {response.status}: {response_bytes.decode('utf-8', errors='replace')}")
Path("response.bin").write_bytes(response_bytes)
finally:
connection.close()
Continue with webhook event schemas and setup, forwarding or AI transformer mappings, depending on whether your destination needs the original email or structured fields.
Running this code needs an API key. Get one free