Phone pools for parallel tests
Documentation navigation
Reserve a phone number for each test worker, wait for its SMS, and release the lease without deleting the number.
A phone pool lets parallel workers borrow different numbers from an existing inventory. Each worker acquires a lease, uses the returned number for its application flow, then releases the lease. The number remains in your account for the next test.
Prepare the pool
Add phone numbers in the dashboard first. Provision enough numbers for the concurrency you intend to run. Creating a lease does not buy a number, and releasing it does not cancel the number's rental.
Create a named pool using createPhonePool or getOrCreatePhonePool. Add only the numbers intended for these tests with addPhoneNumbersToPhonePool. Keep production conversations and unrelated tests outside this pool.
/phone/pools/get-or-create
Get or create phone pool
Get a phone pool by name or create it if it does not exist
Request, parameters, and responses
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | |
description | string | No |
{
"name": "Example name",
"description": "value"
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | PhonePoolDetailDto | OK |
HTTP examples
HTTP
POST /phone/pools/get-or-create HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{
"name": "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 "name": "value"\0012}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/phone/pools/get-or-create' \
--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 \"name\": \"value\"\n}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/phone/pools/get-or-create", {
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/phone/pools/get-or-create")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{\n \"name\": \"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()
/phone/pools/{poolId}/numbers
Add phone numbers to phone pool
Add one or more owned phone numbers to a pool
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
poolId | string:uuid | Yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
phoneNumberIds | string:uuid[] | Yes |
{
"phoneNumberIds": [
"00000000-0000-4000-8000-000000000000"
]
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | PhonePoolDetailDto | OK |
HTTP examples
HTTP
POST /phone/pools/00000000-0000-4000-8000-000000000000/numbers HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{
"phoneNumberIds": [
"00000000-0000-4000-8000-000000000000"
]
}
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 "phoneNumberIds": [\0012 "00000000-0000-4000-8000-000000000000"\0012 ]\0012}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/phone/pools/00000000-0000-4000-8000-000000000000/numbers' \
--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 \"phoneNumberIds\": [\n \"00000000-0000-4000-8000-000000000000\"\n ]\n}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/phone/pools/00000000-0000-4000-8000-000000000000/numbers", {
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/phone/pools/00000000-0000-4000-8000-000000000000/numbers")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{\n \"phoneNumberIds\": [\n \"00000000-0000-4000-8000-000000000000\"\n ]\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()
Reserve, test, and release
Use Node.js 22 or later for this REST example. Set MAILSLURP_API_KEY on the test runner and provide the pool ID. exercisePhone is your application-specific browser flow: it receives the number and the timestamp from before the flow started.
const API_BASE_URL = 'https://api.mailslurp.com';
function createApi(apiKey) {
if (!apiKey) throw new Error('Set MAILSLURP_API_KEY');
return async function api(path, { method = 'GET', body } = {}) {
const response = await fetch(`${API_BASE_URL}${path}`, {
method,
headers: {
'x-api-key': apiKey,
...(body ? { 'content-type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`MailSlurp ${method} ${path} failed (${response.status}): ${detail}`);
}
return response.status === 204 ? null : response.json();
};
}
async function withPhoneLease(api, poolId, workerId, exercisePhone) {
const lease = await api(`/phone/pools/${encodeURIComponent(poolId)}/leases`, {
method: 'POST',
body: {
leaseName: `SMS test for ${workerId}`,
leaseOwner: workerId,
leaseDurationMillis: 10 * 60 * 1000,
acquireTimeoutMillis: 30 * 1000,
},
});
const since = new Date();
try {
return await exercisePhone(lease, since);
} finally {
await api(
`/phone/pools/${encodeURIComponent(poolId)}/leases/${encodeURIComponent(lease.id)}`,
{ method: 'DELETE' },
);
}
}
Inside exercisePhone, enter lease.phoneNumber in the application, trigger the SMS, and call waitForSms or waitForLatestSms with lease.phoneNumberId. Use the supplied since, a bounded timeout, and sender/body conditions where appropriate. Extract the OTP, enter it in the application and assert the final authenticated state.
The lease duration must cover the whole browser flow, message wait and cleanup. An expired lease can make the number available to another worker, so abort work before its expiry rather than continuing with a number that may have been reassigned.
/phone/pools/{poolId}/leases
Acquire phone pool lease
Acquire an available phone number from the pool and mark it leased
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
poolId | string:uuid | Yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
leaseName | string | No | |
leaseOwner | string | No | |
leaseDurationMillis | integer:int64 | No | |
acquireTimeoutMillis | integer:int64 | No |
{
"leaseName": "Example name",
"leaseOwner": "value",
"leaseDurationMillis": 1,
"acquireTimeoutMillis": 1
}
Responses
| Status | Schema | Description |
|---|---|---|
200 | PhonePoolLeaseDto | OK |
HTTP examples
HTTP
POST /phone/pools/00000000-0000-4000-8000-000000000000/leases HTTP/1.1
Host: api.mailslurp.com
x-api-key: YOUR_API_KEY
Content-Type: application/json
Accept: */*
{}
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' '{}'
} > "$request_body"
status=$(curl --silent --show-error \
--request POST \
--url 'https://api.mailslurp.com/phone/pools/00000000-0000-4000-8000-000000000000/leases' \
--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("{}", "utf8")
]);
const response = await fetch("https://api.mailslurp.com/phone/pools/00000000-0000-4000-8000-000000000000/leases", {
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/phone/pools/00000000-0000-4000-8000-000000000000/leases")
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
"Accept": "*/*"
}
body = b"".join([
"{}".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()
/phone/pools/{poolId}/leases/{leaseId}
Release phone pool lease
Release an active phone pool lease
Request, parameters, and responses
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
poolId | string:uuid | Yes | |
leaseId | string:uuid | Yes |
Responses
| Status | Schema | Description |
|---|---|---|
204 | Response | No Content |
HTTP examples
HTTP
DELETE /phone/pools/00000000-0000-4000-8000-000000000000/leases/00000000-0000-4000-8000-000000000000 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 DELETE \
--url 'https://api.mailslurp.com/phone/pools/00000000-0000-4000-8000-000000000000/leases/00000000-0000-4000-8000-000000000000' \
--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/phone/pools/00000000-0000-4000-8000-000000000000/leases/00000000-0000-4000-8000-000000000000", {
method: "DELETE",
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/phone/pools/00000000-0000-4000-8000-000000000000/leases/00000000-0000-4000-8000-000000000000")
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("DELETE", 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()
Pool exhaustion and test retries
acquireTimeoutMillis bounds how long acquisition can wait for a free number; it is separate from the SMS wait timeout. If acquisition fails, report a resource/setup failure and do not pick an arbitrary shared number. Reduce parallelism or add numbers to the pool.
Give leaseOwner a worker/run identifier so a failed job is traceable. A retried test needs a new timestamp and a newly acquired lease. Do not reuse an earlier OTP or assume unread state alone isolates the test. If the process crashes before cleanup, the lease expires at expiresAt.
Use the pool from a browser framework
Put lease acquisition in a fixture or setup method and release in teardown or finally. Keep the lease in the message-service object rather than the page object, which should only operate the application UI.
Running this code needs an API key. Get one free