MailSlurp logo

Automate inbox placement and review reports

Documentation navigation
Search documentation

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.

POST /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)

FieldTypeRequiredDescription
seedlistProfilestringNo
requestedSegmentenum: ALL_INBOXES | PERSONAL_INBOXES | PROFESSIONAL_INBOXESYesValues: ALL_INBOXES, PERSONAL_INBOXES, PROFESSIONAL_INBOXES
senderDomainstringNo
fromEmailstringNo
publicShareRequestedbooleanYes
addressFormatenum: COMMA | NEWLINEYesValues: COMMA, NEWLINE
Request example
{
  "requestedSegment": "ALL_INBOXES",
  "publicShareRequested": true,
  "addressFormat": "COMMA",
  "seedlistProfile": "value",
  "senderDomain": "example.com",
  "fromEmail": "user@example.com"
}

Responses

StatusSchemaDescription
200InboxPlacementTestRunDtoOK
HTTP examples

HTTP

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.

cURL
(
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.

JavaScript (HTTP)
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.

Python (HTTP)
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()
POST /inbox-placement-tests

Create a new direct-send inbox placement test

Request, parameters, and responses

Request body (required)

FieldTypeRequiredDescription
seedlistProfilestringNo
requestedSegmentenum: ALL_INBOXES | PERSONAL_INBOXES | PROFESSIONAL_INBOXESYesValues: ALL_INBOXES, PERSONAL_INBOXES, PROFESSIONAL_INBOXES
senderDomainstringNo
fromEmailstringNo
publicShareRequestedbooleanYes
addressFormatenum: COMMA | NEWLINEYesValues: COMMA, NEWLINE
Request example
{
  "requestedSegment": "ALL_INBOXES",
  "publicShareRequested": true,
  "addressFormat": "COMMA",
  "seedlistProfile": "value",
  "senderDomain": "example.com",
  "fromEmail": "user@example.com"
}

Responses

StatusSchemaDescription
200InboxPlacementTestRunDtoOK
HTTP examples

HTTP

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.

cURL
(
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.

JavaScript (HTTP)
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.

Python (HTTP)
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.

POST /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

NameTypeRequiredDescription
idstring:uuidYes

Responses

StatusSchemaDescription
200InboxPlacementTestRunDtoOK
HTTP examples

HTTP

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.

cURL
(
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.

JavaScript (HTTP)
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.

Python (HTTP)
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,
});
GET /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

NameTypeRequiredDescription
idstring:uuidYes

Responses

StatusSchemaDescription
200ResponseOK
HTTP examples

HTTP

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.

cURL
(
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.

JavaScript (HTTP)
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.

Python (HTTP)
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()
GET /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

NameTypeRequiredDescription
idstring:uuidYes

Query parameters

NameTypeRequiredDescription
providerenum: GENERIC | MAILCHIMP | KLAVIYO | SALESFORCE | HUBSPOT | BRAZE | BREVO | MAILERLITE | MAILJET | OMNISEND | GETRESPONSE | CONSTANT_CONTACT | KITNoValues: GENERIC, MAILCHIMP, KLAVIYO, SALESFORCE, HUBSPOT, BRAZE, BREVO, MAILERLITE, MAILJET, OMNISEND, GETRESPONSE, CONSTANT_CONTACT, KIT

Responses

StatusSchemaDescription
200ResponseOK
HTTP examples

HTTP

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.

cURL
(
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.

JavaScript (HTTP)
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.

Python (HTTP)
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()
GET /inbox-placement-tests/share/{shareToken}

Get a public inbox placement share

Request, parameters, and responses

Path parameters

NameTypeRequiredDescription
shareTokenstringYes

Responses

StatusSchemaDescription
200InboxPlacementPublicShareDtoOK
HTTP examples

HTTP

HTTP
GET /inbox-placement-tests/share/value 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.

cURL
(
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/share/value' \
  --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.

JavaScript (HTTP)
import { writeFile } from "node:fs/promises";

const response = await fetch("https://api.mailslurp.com/inbox-placement-tests/share/value", {
  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.

Python (HTTP)
import http.client
from pathlib import Path
from urllib.parse import urlsplit

url = urlsplit("https://api.mailslurp.com/inbox-placement-tests/share/value")
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()

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.