MailSlurp logo

Phone pools for parallel tests

Documentation navigation
Search documentation

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.

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

GetOrCreatePhonePoolOptions application/json
FieldTypeRequiredDescription
namestringYes
descriptionstringNo
Request example
{
  "name": "Example name",
  "description": "value"
}

Responses

StatusSchemaDescription
200PhonePoolDetailDtoOK
HTTP examples

HTTP

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.

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

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

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

NameTypeRequiredDescription
poolIdstring:uuidYes

Request body (required)

AddPhonePoolNumbersOptions application/json
FieldTypeRequiredDescription
phoneNumberIdsstring:uuid[]Yes
Request example
{
  "phoneNumberIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}

Responses

StatusSchemaDescription
200PhonePoolDetailDtoOK
HTTP examples

HTTP

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.

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

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

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

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

NameTypeRequiredDescription
poolIdstring:uuidYes

Request body (required)

AcquirePhonePoolLeaseOptions application/json
FieldTypeRequiredDescription
leaseNamestringNo
leaseOwnerstringNo
leaseDurationMillisinteger:int64No
acquireTimeoutMillisinteger:int64No
Request example
{
  "leaseName": "Example name",
  "leaseOwner": "value",
  "leaseDurationMillis": 1,
  "acquireTimeoutMillis": 1
}

Responses

StatusSchemaDescription
200PhonePoolLeaseDtoOK
HTTP examples

HTTP

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.

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

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

Python (HTTP)
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()
DELETE /phone/pools/{poolId}/leases/{leaseId}

Release phone pool lease

Release an active phone pool lease

Request, parameters, and responses

Path parameters

NameTypeRequiredDescription
poolIdstring:uuidYes
leaseIdstring:uuidYes

Responses

StatusSchemaDescription
204ResponseNo Content
HTTP examples

HTTP

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.

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

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

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