Automate inbox placement and review reports
Documentation navigation
Create placement runs, send the current tracking marker, wait for completion and compare provider results over time.
Automate a placement test when a campaign, template or sender changes. Send through the same ESP and sender identity used for the campaign so the result measures that delivery path. Start with the manual workflow if you are testing a provider for the first time.
Run a placement test from JavaScript, Python, Java or C#
These steps create a test, send your campaign to its seed inboxes, wait for the result and count where each provider placed the email. Choose a language once and every example on the page switches to it. The complete placement examples include dependency files for each language.
Create a client
Install the SDK for your language and set MAILSLURP_API_KEY in your server environment. The C# examples call the placement REST endpoints directly because they are newer than the current NuGet package.
JavaScript
import {
ApiInboxPlacementTestControllerApi,
Configuration,
CreateInboxPlacementTestOptionsAddressFormatEnum,
CreateInboxPlacementTestOptionsRequestedSegmentEnum,
MailSlurp,
} from 'mailslurp-client';
export function createClients() {
const apiKey = process.env.MAILSLURP_API_KEY;
if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');
return {
mailslurp: new MailSlurp({ apiKey }),
placement: new ApiInboxPlacementTestControllerApi(new Configuration({ apiKey })),
};
}
Python
import os
import time
from collections import Counter, defaultdict
import mailslurp_client
def create_client():
api_key = os.environ.get("MAILSLURP_API_KEY")
if not api_key:
raise RuntimeError("Set MAILSLURP_API_KEY")
configuration = mailslurp_client.Configuration()
configuration.api_key["x-api-key"] = api_key
return mailslurp_client.ApiClient(configuration)
Java
import com.mailslurp.apis.*;
import com.mailslurp.clients.ApiClient;
import com.mailslurp.clients.ApiException;
import com.mailslurp.models.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
final class PlacementSetup {
static ApiClient createClient() {
String apiKey = System.getenv("MAILSLURP_API_KEY");
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalStateException("Set MAILSLURP_API_KEY");
}
ApiClient client = new ApiClient();
client.setApiKey(apiKey);
client.setConnectTimeout(10_000);
client.setReadTimeout(75_000);
return client;
}
}
C#
// The inbox placement endpoints are newer than the current NuGet SDK, so these helpers call the
// REST API with HttpClient. Sending uses the MailSlurp SDK. Both use your x-api-key.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using mailslurp.Api;
using mailslurp.Client;
using mailslurp.Model;
public static class PlacementSetup
{
public static HttpClient CreateHttpClient()
{
var http = new HttpClient { BaseAddress = new Uri("https://api.mailslurp.com"), Timeout = TimeSpan.FromSeconds(75) };
http.DefaultRequestHeaders.Add("x-api-key", ApiKey());
return http;
}
public static Configuration CreateConfiguration()
{
var configuration = new Configuration { Timeout = TimeSpan.FromSeconds(75) };
configuration.ApiKey.Add("x-api-key", ApiKey());
return configuration;
}
private static string ApiKey()
{
var apiKey = Environment.GetEnvironmentVariable("MAILSLURP_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
throw new InvalidOperationException("Set MAILSLURP_API_KEY");
return apiKey;
}
}
Create a test
A test gives you seed addresses at Gmail, Outlook, Yahoo and other providers, plus a tracking token for the campaign:
JavaScript
export async function createTest(placement, senderDomain, fromEmail) {
// A test gives you seed addresses at Gmail, Outlook, Yahoo and other providers.
const run = await placement.createInboxPlacementTest({
createInboxPlacementTestOptions: {
requestedSegment: CreateInboxPlacementTestOptionsRequestedSegmentEnum.ALL_INBOXES,
senderDomain,
fromEmail,
publicShareRequested: false,
addressFormat: CreateInboxPlacementTestOptionsAddressFormatEnum.COMMA,
},
});
// Keep the tracking token: the campaign you send must include it.
return { testId: run.id, trackingToken: run.trackingToken, seedAddresses: run.seedAddresses };
}
Python
def create_test(api_client, sender_domain, from_email):
# A test gives you seed addresses at Gmail, Outlook, Yahoo and other providers.
run = mailslurp_client.ApiInboxPlacementTestControllerApi(api_client).create_inbox_placement_test(
mailslurp_client.CreateInboxPlacementTestOptions(
requested_segment="ALL_INBOXES",
sender_domain=sender_domain,
from_email=from_email,
public_share_requested=False,
address_format="COMMA",
)
)
# Keep the tracking token: the campaign you send must include it.
return run.id, run.tracking_token, run.seed_addresses
Java
public static InboxPlacementTestRunDto createTest(ApiClient client, String senderDomain, String fromEmail) throws ApiException {
// A test gives you seed addresses at Gmail, Outlook, Yahoo and other providers, and a
// tracking token that the campaign you send must include.
return new ApiInboxPlacementTestControllerApi(client).createInboxPlacementTest(new CreateInboxPlacementTestOptions()
.requestedSegment(CreateInboxPlacementTestOptions.RequestedSegmentEnum.ALL_INBOXES)
.senderDomain(senderDomain)
.fromEmail(fromEmail)
.publicShareRequested(false)
.addressFormat(CreateInboxPlacementTestOptions.AddressFormatEnum.COMMA)).execute();
}
C#
public static async Task<(string TestId, string TrackingToken, List<string> SeedAddresses)> CreateTest(
HttpClient http, string senderDomain, string fromEmail)
{
// A test gives you seed addresses at Gmail, Outlook, Yahoo and other providers, and a
// tracking token that the campaign you send must include.
var response = await http.PostAsJsonAsync("/inbox-placement-tests", new
{
requestedSegment = "ALL_INBOXES",
senderDomain,
fromEmail,
publicShareRequested = false,
addressFormat = "COMMA",
});
response.EnsureSuccessStatusCode();
var run = await response.Content.ReadFromJsonAsync<JsonElement>();
var seeds = run.GetProperty("seedAddresses").EnumerateArray().Select(seed => seed.GetString()).ToList();
return (run.GetProperty("id").GetString(), run.GetProperty("trackingToken").GetString(), seeds);
}
Send to the seeds and confirm the send
Send the campaign to every seed address with the tracking token in the subject, then confirm the send so MailSlurp starts watching the seed inboxes:
JavaScript
export async function sendToSeeds(mailslurp, placement, senderInboxId, test) {
// Send your real campaign to every seed address with the test's tracking token in the subject.
// In production, send through your own ESP from the domain you are testing; a MailSlurp inbox
// is used here to keep the example short.
await mailslurp.sendEmail(senderInboxId, {
to: test.seedAddresses,
subject: `Spring sale: 20% off everything ${test.trackingToken}`,
body: '<h1>Spring sale</h1><p>20% off everything this week.</p>',
isHTML: true,
});
// Confirm only after the send succeeded, so MailSlurp starts watching the seed inboxes.
await placement.confirmInboxPlacementTestSent({ id: test.testId });
}
Python
def send_to_seeds(api_client, sender_inbox_id, test_id, tracking_token, seed_addresses):
# Send your real campaign to every seed address with the test's tracking token in the subject.
# In production, send through your own ESP from the domain you are testing; a MailSlurp inbox
# is used here to keep the example short.
mailslurp_client.InboxControllerApi(api_client).send_email(
sender_inbox_id,
mailslurp_client.SendEmailOptions(
to=seed_addresses,
subject=f"Spring sale: 20% off everything {tracking_token}",
body="<h1>Spring sale</h1><p>20% off everything this week.</p>",
is_html=True,
),
)
# Confirm only after the send succeeded, so MailSlurp starts watching the seed inboxes.
mailslurp_client.ApiInboxPlacementTestControllerApi(api_client).confirm_inbox_placement_test_sent(test_id)
Java
public static void sendToSeeds(ApiClient client, UUID senderInboxId, InboxPlacementTestRunDto test) throws ApiException {
// Send your real campaign to every seed address with the test's tracking token in the subject.
// In production, send through your own ESP from the domain you are testing; a MailSlurp inbox
// is used here to keep the example short.
new InboxControllerApi(client).sendEmail(senderInboxId, new SendEmailOptions()
.to(test.getSeedAddresses())
.subject("Spring sale: 20% off everything " + test.getTrackingToken())
.body("<h1>Spring sale</h1><p>20% off everything this week.</p>")
.isHTML(true)).execute();
// Confirm only after the send succeeded, so MailSlurp starts watching the seed inboxes.
new ApiInboxPlacementTestControllerApi(client).confirmInboxPlacementTestSent(test.getId()).execute();
}
C#
public static async Task SendToSeeds(Configuration configuration, HttpClient http, Guid senderInboxId,
string testId, string trackingToken, List<string> seedAddresses)
{
// Send your real campaign to every seed address with the test's tracking token in the subject.
// In production, send through your own ESP from the domain you are testing; a MailSlurp inbox
// is used here to keep the example short.
new InboxControllerApi(configuration).SendEmail(senderInboxId, new SendEmailOptions(
to: seedAddresses,
subject: $"Spring sale: 20% off everything {trackingToken}",
body: "<h1>Spring sale</h1><p>20% off everything this week.</p>",
isHTML: true));
// Confirm only after the send succeeded, so MailSlurp starts watching the seed inboxes.
var confirmed = await http.PostAsync($"/inbox-placement-tests/{testId}/confirm-sent", null);
confirmed.EnsureSuccessStatusCode();
}
Wait for the result
Seeds report as messages arrive. Poll until the test completes, which usually takes a few minutes:
JavaScript
export async function waitForResults(placement, testId) {
// Seeds report as messages arrive; poll until the test completes (usually a few minutes).
for (let attempt = 0; attempt < 40; attempt += 1) {
const run = await placement.getInboxPlacementTest({ id: testId });
if (run.completedAt) {
return { inbox: run.inboxTargets, spam: run.spamTargets, missing: run.notReceivedTargets, total: run.totalTargets };
}
await new Promise(resolve => setTimeout(resolve, 15_000));
}
throw new Error(`Inbox placement test ${testId} is still running`);
}
Python
def wait_for_results(api_client, test_id):
# Seeds report as messages arrive; poll until the test completes (usually a few minutes).
placement = mailslurp_client.ApiInboxPlacementTestControllerApi(api_client)
for _ in range(40):
run = placement.get_inbox_placement_test(test_id)
if run.completed_at:
return {"inbox": run.inbox_targets, "spam": run.spam_targets,
"missing": run.not_received_targets, "total": run.total_targets}
time.sleep(15)
raise TimeoutError(f"Inbox placement test {test_id} is still running")
Java
public static InboxPlacementTestRunDto waitForResults(ApiClient client, UUID testId) throws Exception {
// Seeds report as messages arrive; poll until the test completes (usually a few minutes).
ApiInboxPlacementTestControllerApi placement = new ApiInboxPlacementTestControllerApi(client);
for (int attempt = 0; attempt < 40; attempt++) {
InboxPlacementTestRunDto run = placement.getInboxPlacementTest(testId).execute();
if (run.getCompletedAt() != null) {
System.out.println("Inbox " + run.getInboxTargets() + ", spam " + run.getSpamTargets()
+ ", missing " + run.getNotReceivedTargets());
return run;
}
Thread.sleep(15_000);
}
throw new IllegalStateException("Inbox placement test " + testId + " is still running");
}
C#
public static async Task<JsonElement> WaitForResults(HttpClient http, string testId)
{
// Seeds report as messages arrive; poll until the test completes (usually a few minutes).
for (var attempt = 0; attempt < 40; attempt++)
{
var run = await http.GetFromJsonAsync<JsonElement>($"/inbox-placement-tests/{testId}");
if (run.TryGetProperty("completedAt", out var completedAt) && completedAt.ValueKind != JsonValueKind.Null)
{
Console.WriteLine($"Inbox {run.GetProperty("inboxTargets")}, spam {run.GetProperty("spamTargets")}, " +
$"missing {run.GetProperty("notReceivedTargets")}");
return run;
}
await Task.Delay(TimeSpan.FromSeconds(15));
}
throw new TimeoutException($"Inbox placement test {testId} is still running");
}
Count placement by provider
Group the per-seed results to see which providers put the email in the inbox, in spam or did not receive it:
JavaScript
export async function placementByProvider(placement, testId) {
// Count where each provider put the email: INBOX, SPAM or not received.
const { results } = await placement.getInboxPlacementTestResults({ id: testId });
const byProvider = {};
for (const target of results?.targets ?? []) {
const folder = target.placementFolder ?? 'NOT_RECEIVED';
byProvider[target.provider] ??= {};
byProvider[target.provider][folder] = (byProvider[target.provider][folder] ?? 0) + 1;
}
return byProvider;
}
Python
def placement_by_provider(api_client, test_id):
# Count where each provider put the email: INBOX, SPAM or not received.
results = mailslurp_client.ApiInboxPlacementTestControllerApi(
api_client
).get_inbox_placement_test_results(test_id).results
by_provider = defaultdict(Counter)
for target in (results.targets if results else []):
by_provider[target.provider][target.placement_folder or "NOT_RECEIVED"] += 1
return dict(by_provider)
Java
public static Map<String, Map<String, Integer>> placementByProvider(ApiClient client, UUID testId) throws ApiException {
// Count where each provider put the email: INBOX, SPAM or not received.
InboxPlacementRemoteResultsDto results = new ApiInboxPlacementTestControllerApi(client)
.getInboxPlacementTestResults(testId).execute().getResults();
Map<String, Map<String, Integer>> byProvider = new HashMap<>();
if (results == null) return byProvider;
for (InboxPlacementTargetResultDto target : results.getTargets()) {
String folder = target.getPlacementFolder() == null ? "NOT_RECEIVED" : target.getPlacementFolder();
byProvider.computeIfAbsent(target.getProvider(), key -> new HashMap<>()).merge(folder, 1, Integer::sum);
}
return byProvider;
}
C#
public static async Task<Dictionary<string, Dictionary<string, int>>> PlacementByProvider(HttpClient http, string testId)
{
// Count where each provider put the email: INBOX, SPAM or not received.
var response = await http.GetFromJsonAsync<JsonElement>($"/inbox-placement-tests/{testId}/results");
var byProvider = new Dictionary<string, Dictionary<string, int>>();
if (!response.TryGetProperty("results", out var results) || results.ValueKind == JsonValueKind.Null) return byProvider;
foreach (var target in results.GetProperty("targets").EnumerateArray())
{
var provider = target.GetProperty("provider").GetString();
var folder = target.TryGetProperty("placementFolder", out var value) && value.ValueKind == JsonValueKind.String
? value.GetString() : "NOT_RECEIVED";
if (!byProvider.TryGetValue(provider, out var counts)) byProvider[provider] = counts = new Dictionary<string, int>();
counts[folder] = counts.TryGetValue(folder, out var count) ? count + 1 : 1;
}
return byProvider;
}
Create a run and use its current seed list
The JavaScript example uses mailslurp-client and its ApiInboxPlacementTestControllerApi. Define YOUR_API_KEY, YOUR_DOMAIN and YOUR_FROM_EMAIL from server-side configuration.
import { MailSlurp, Configuration, ApiInboxPlacementTestControllerApi } from 'mailslurp-client';
import { setTimeout as sleep } from 'node:timers/promises';
const YOUR_API_KEY = process.env.MAILSLURP_API_KEY;
const YOUR_DOMAIN = 'example.com';
const YOUR_FROM_EMAIL = 'hello@example.com';
const YOUR_SENDER_INBOX_ID = process.env.MAILSLURP_SENDER_INBOX_ID;
const mailslurp = new MailSlurp({ apiKey: YOUR_API_KEY });
const inboxPlacementController = new ApiInboxPlacementTestControllerApi(
new Configuration({ apiKey: YOUR_API_KEY }),
);
const inboxPlacementRun = await inboxPlacementController.createInboxPlacementTest({
createInboxPlacementTestOptions: {
requestedSegment: "ALL_INBOXES",
senderDomain: YOUR_DOMAIN,
fromEmail: YOUR_FROM_EMAIL,
publicShareRequested: false,
addressFormat: "COMMA",
},
});
console.log({ id: inboxPlacementRun.id, totalTargets: inboxPlacementRun.totalTargets });
Keep the run ID, trackingToken and seedAddresses together. Each test supplies its own marker and targets; do not reuse values from an earlier test. Use prepareInboxPlacementTest when you want to inspect preparation before consuming a test allowance.
/inbox-placement-tests/prepare
Prepare a direct-send inbox placement test without consuming allowance
Reserve seed addresses for the dashboard create flow. The run remains staged and allowance is not committed until send confirmation. Reuses a recent active preparation when possible.
Request, parameters, and responses
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
seedlistProfile | string | No | |
requestedSegment | enum: ALL_INBOXES | PERSONAL_INBOXES | PROFESSIONAL_INBOXES | Yes | Values: ALL_INBOXES, PERSONAL_INBOXES, PROFESSIONAL_INBOXES |
senderDomain | string | No | |
fromEmail | string | No | |
publicShareRequested | boolean | Yes | |
addressFormat | enum: COMMA | NEWLINE | Yes | Values: COMMA, NEWLINE |
{
"requestedSegment": "ALL_INBOXES",
"publicShareRequested": true,
"addressFormat": "COMMA",
"seedlistProfile": "value",
"senderDomain": "example.com",
"fromEmail": "user@example.com"
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | InboxPlacementTestRunDto | OK |
HTTP examples
HTTP
POST /inbox-placement-tests/prepare HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{
"requestedSegment": "ALL_INBOXES",
"publicShareRequested": false,
"addressFormat": "COMMA"
}
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 "requestedSegment": "ALL_INBOXES",\0012 "publicShareRequested": false,\0012 "addressFormat": "COMMA"\0012}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/inbox-placement-tests/prepare' \
--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 \"requestedSegment\": \"ALL_INBOXES\",\n \"publicShareRequested\": false,\n \"addressFormat\": \"COMMA\"\n}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/inbox-placement-tests/prepare", {
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/inbox-placement-tests/prepare")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{\n \"requestedSegment\": \"ALL_INBOXES\",\n \"publicShareRequested\": false,\n \"addressFormat\": \"COMMA\"\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()
/inbox-placement-tests
Create a new direct-send inbox placement test
Request, parameters, and responses
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
seedlistProfile | string | No | |
requestedSegment | enum: ALL_INBOXES | PERSONAL_INBOXES | PROFESSIONAL_INBOXES | Yes | Values: ALL_INBOXES, PERSONAL_INBOXES, PROFESSIONAL_INBOXES |
senderDomain | string | No | |
fromEmail | string | No | |
publicShareRequested | boolean | Yes | |
addressFormat | enum: COMMA | NEWLINE | Yes | Values: COMMA, NEWLINE |
{
"requestedSegment": "ALL_INBOXES",
"publicShareRequested": true,
"addressFormat": "COMMA",
"seedlistProfile": "value",
"senderDomain": "example.com",
"fromEmail": "user@example.com"
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | InboxPlacementTestRunDto | OK |
HTTP examples
HTTP
POST /inbox-placement-tests HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{
"requestedSegment": "ALL_INBOXES",
"publicShareRequested": false,
"addressFormat": "COMMA"
}
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 "requestedSegment": "ALL_INBOXES",\0012 "publicShareRequested": false,\0012 "addressFormat": "COMMA"\0012}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/inbox-placement-tests' \
--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 \"requestedSegment\": \"ALL_INBOXES\",\n \"publicShareRequested\": false,\n \"addressFormat\": \"COMMA\"\n}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/inbox-placement-tests", {
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/inbox-placement-tests")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{\n \"requestedSegment\": \"ALL_INBOXES\",\n \"publicShareRequested\": false,\n \"addressFormat\": \"COMMA\"\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()
Send and confirm
Put the exact tracking token in the subject or body and send to every supplied seed address. The following example uses a MailSlurp sender inbox identified by YOUR_SENDER_INBOX_ID. For an ESP campaign, perform this send through that ESP instead.
if (!inboxPlacementRun.trackingToken || !inboxPlacementRun.seedAddresses.length) {
throw new Error("The placement run must provide a tracking marker and recipients");
}
await mailslurp.sendEmail(YOUR_SENDER_INBOX_ID, {
to: inboxPlacementRun.seedAddresses,
subject: `Placement check ${inboxPlacementRun.trackingToken}`,
body: "This message checks the configured sender's inbox placement.",
isHTML: false,
});
await inboxPlacementController.confirmInboxPlacementTestSent({ id: inboxPlacementRun.id });
The confirmation runs only after message submission succeeds. Keep send failure separate from placement failure: an ESP rejecting the request is different from a successfully sent message reaching spam.
/inbox-placement-tests/{id}/confirm-sent
Confirm an inbox placement test email has been sent
Reconcile any messages that arrived after seed reservation, commit allowance, and move a prepared dashboard test into its active lifecycle. This operation is idempotent.
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string:uuid | Yes |
Responses
| Status | Schema | Description |
|---|---|---|
200 | InboxPlacementTestRunDto | OK |
HTTP examples
HTTP
POST /inbox-placement-tests/00000000-0000-4000-8000-000000000000/confirm-sent 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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/confirm-sent' \
--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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/confirm-sent", {
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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/confirm-sent")
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()
Wait for the complete result
Poll with a deadline. A matched target means some evidence has arrived; it does not mean every provider has completed its observation window.
async function waitForInboxPlacementResults(testId, timeoutMs = 300_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const run = await inboxPlacementController.getInboxPlacementTest({ id: testId });
if (run.status === "FAILED") throw new Error(`Placement test ${testId} failed`);
if (run.status === "COMPLETE") {
return inboxPlacementController.getInboxPlacementTestResults({ id: testId });
}
await sleep(Math.min(15_000, Math.max(0, deadline - Date.now())));
}
throw new Error(`Placement test ${testId} is still pending; inspect it later`);
}
const inboxPlacementResults = await waitForInboxPlacementResults(inboxPlacementRun.id);
console.log(inboxPlacementResults.results?.deliverability);
The example allows five minutes. Set the deadline according to your test window and retain the run ID if your local deadline expires, so you can inspect the run later. Do not turn a still-pending provider into an assumed inbox or missing outcome.
const targetResults = inboxPlacementResults.results?.targets ?? [];
const placementsByProvider = {};
for (const target of targetResults) {
const provider = target.provider || "UNKNOWN_PROVIDER";
const folder = target.placementFolder || "UNRESOLVED";
placementsByProvider[provider] ??= {};
placementsByProvider[provider][folder] = (placementsByProvider[provider][folder] ?? 0) + 1;
}
console.log(placementsByProvider);
Preserve each returned placement value, including Promotions and any unresolved state. Keep sender, campaign revision and audience segment with the result when comparing tests.
Schedule recurring checks
Open Inbox placement > Automations > Create automation. Select a verified sending connection, then set a name, Frequency and Inbox audience. Enter the subject and text body, preserving {{trackingToken}} in at least one of them so MailSlurp can insert a fresh marker for every run. Review the sender and allowance, then create the automation.
Reuse a verified connection from email warmup where appropriate. Provider setup guides cover Resend, Mailgun, Postmark, SendGrid, Brevo, SMTP2GO, Mailjet and MailerSend.
Run now submits an immediate test. A later scheduled execution confirms recurrence; inspect its own linked report and wait for all targets to settle. A completed send is distinct from the received inbox, tab or spam classification. Provider logs establish acceptance and delivery, while the placement report records the observed folders.
The configured subject and text body are the tested message. A separately authored ESP campaign may have different HTML, links or routing; send that campaign through its original path when you need to measure its placement. Always use the current run's full seed list rather than assuming a fixed number of recipients.
Warmup and automated placement share a Delivery Sender seat. Pausing an automation preserves history and retains the sender allocation. Stop other workflows using the connection before disabling it to release capacity. Each submitted placement test uses the relevant test allowance or meter; every seed recipient also counts toward the ESP's sending limits. Warmup checkpoints use warmup credits instead of consuming a separate placement test.
Review the sender's test history after credential, template or DNS changes. Configure available Slack or Teams placement alerts in the placement workflow and verify the destination. For a custom scheduler, use the public create/send/confirm/results workflow above on each run, obtaining a fresh seed set and marker each time.
Compare, share and export
Use analytics with the same sender filters and comparable time periods. A changed seed audience or campaign is context for a difference, not proof of a sender regression by itself.
// Fetch aggregate inbox placement trends for your sender domain.
const before = new Date();
const since = new Date(before.getTime() - 30 * 24 * 60 * 60 * 1000);
const breakdown =
await inboxPlacementController.getInboxPlacementAnalyticsBreakdown({
since,
before,
senderDomain: YOUR_DOMAIN,
fromEmail: YOUR_FROM_EMAIL,
limit: 10,
});
const series = await inboxPlacementController.getInboxPlacementAnalyticsSeries({
since,
before,
bucket: "DAY",
groupBy: "SENDER_DOMAIN",
senderDomain: YOUR_DOMAIN,
runLimit: 100,
groupLimit: 5,
});
console.log({
summary: breakdown.summary,
folders: breakdown.folders,
points: series.points,
});
/inbox-placement-tests/{id}/results/export
Export inbox placement test results as CSV
Export one row per placement target, including run-level delivery totals, 0-10 placement scores, and target placement outcome fields.
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string:uuid | Yes |
Responses
| Status | Schema | Description |
|---|---|---|
200 | Response | OK |
HTTP examples
HTTP
GET /inbox-placement-tests/00000000-0000-4000-8000-000000000000/results/export 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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/results/export' \
--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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/results/export", {
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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/results/export")
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()
/inbox-placement-tests/{id}/seed-list.csv
Download inbox placement seed addresses as an email-platform CSV
Export the current run's seed addresses with headers recognized by the selected email platform. Downloading a prepared seed list does not confirm the send or consume allowance.
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string:uuid | Yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
provider | enum: GENERIC | MAILCHIMP | KLAVIYO | SALESFORCE | HUBSPOT | BRAZE | BREVO | MAILERLITE | MAILJET | OMNISEND | GETRESPONSE | CONSTANT_CONTACT | KIT | No | Values: GENERIC, MAILCHIMP, KLAVIYO, SALESFORCE, HUBSPOT, BRAZE, BREVO, MAILERLITE, MAILJET, OMNISEND, GETRESPONSE, CONSTANT_CONTACT, KIT |
Responses
| Status | Schema | Description |
|---|---|---|
200 | Response | OK |
HTTP examples
HTTP
GET /inbox-placement-tests/00000000-0000-4000-8000-000000000000/seed-list.csv 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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/seed-list.csv' \
--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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/seed-list.csv", {
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/inbox-placement-tests/00000000-0000-4000-8000-000000000000/seed-list.csv")
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()
/inbox-placement-tests/share/{shareToken}
Get a public inbox placement share
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
shareToken | string | Yes |
Responses
| Status | Schema | Description |
|---|---|---|
200 | InboxPlacementPublicShareDto | OK |
HTTP examples
Export the result CSV for analysis and use the seed-list CSV to prepare recipient imports. Request public sharing only when the campaign is suitable for external review. Inspect content and domain findings, then follow up with domain monitoring or Email Audit for the issue found.
Running this code needs an API key. Get one free