Use Xitroo from your own code: receive confirmation mails in end-to-end tests,
automate sign-up flows, or send e-mails from any @xitroo.com address.
Pick Developer, Team or Business below and click Subscribe.
Card or other payment methods. Monthly in US dollars, invoice by e-mail, cancel anytime.
Your key (xit_…) appears right after payment and is also sent to your e-mail address.
Lost your key? Request a new one →
plus applicable sales tax / VAT, billed monthly in US dollars, cancel anytime
Change plan, update payment details, download invoices or cancel. Enter your API key.
Enter the e-mail address of your subscription. We send you a new key; the old one stops working.
xit_.X-Api-Key with every request.@xitroo.com, @xitroo.de or @xitroo.fr works, no setup needed. Mails are deleted after 48 hours.curl -H "X-Api-Key: $XITROO_API_KEY" \
"https://api.xitroo.com/v1/mails?mailAddress=my-test-42@xitroo.com&mailsPerPage=25&minTimestamp=0&maxTimestamp=$(date +%s)&locale=en"
Base URL: https://api.xitroo.com/v1 · all responses are JSON · keys never go into the URL.
Tested code in eight languages. Set the environment variable XITROO_API_KEY to your key.
ADDRESS="my-test-42@xitroo.com"
SINCE=$(date +%s)
# ... sign up with $ADDRESS in your app ...
while :; do
sleep 2
ID=$(curl -s -H "X-Api-Key: $XITROO_API_KEY" \
"https://api.xitroo.com/v1/mails?mailAddress=$ADDRESS&mailsPerPage=10&minTimestamp=$SINCE&maxTimestamp=$(( $(date +%s) + 60 ))&locale=en" \
| jq -r '.mails[0]._id // empty')
[ -n "$ID" ] && break
done
curl -s -H "X-Api-Key: $XITROO_API_KEY" \
"https://api.xitroo.com/v1/mail?id=$ID&locale=en" | jq -r .subject
<?php
$api = 'https://api.xitroo.com/v1';
$ctx = stream_context_create(['http' => [
'header' => 'X-Api-Key: ' . getenv('XITROO_API_KEY'),
'ignore_errors' => true,
]]);
$address = 'my-test-42@xitroo.com';
$since = time();
// ... sign up with $address in your app ...
do {
sleep(2);
$query = http_build_query([
'mailAddress' => $address, 'mailsPerPage' => 10, 'locale' => 'en',
'minTimestamp' => $since, 'maxTimestamp' => time() + 60,
]);
$list = json_decode(file_get_contents("$api/mails?$query", false, $ctx), true);
} while (empty($list['mails']));
$id = $list['mails'][0]['_id'];
$mail = json_decode(file_get_contents("$api/mail?id=$id&locale=en", false, $ctx), true);
echo $mail['subject'], PHP_EOL;
// Node.js 18+, save as wait.mjs
const API = "https://api.xitroo.com/v1";
const headers = { "X-Api-Key": process.env.XITROO_API_KEY };
const address = "my-test-42@xitroo.com";
const since = Math.floor(Date.now() / 1000);
// ... sign up with address in your app ...
let list;
do {
await new Promise((r) => setTimeout(r, 2000));
const q = new URLSearchParams({
mailAddress: address, mailsPerPage: 10, locale: "en",
minTimestamp: since, maxTimestamp: Math.floor(Date.now() / 1000) + 60,
});
list = await (await fetch(`${API}/mails?${q}`, { headers })).json();
} while (!list.mails);
const id = list.mails[0]._id;
const mail = await (await fetch(`${API}/mail?id=${id}&locale=en`, { headers })).json();
console.log(mail.subject);
import os, time, requests
API = "https://api.xitroo.com/v1"
HEADERS = {"X-Api-Key": os.environ["XITROO_API_KEY"]}
address = "my-test-42@xitroo.com"
since = int(time.time())
# ... sign up with address in your app ...
while True:
time.sleep(2)
data = requests.get(f"{API}/mails", headers=HEADERS, params={
"mailAddress": address, "mailsPerPage": 10, "locale": "en",
"minTimestamp": since, "maxTimestamp": int(time.time()) + 60}).json()
if data.get("mails"):
break
mail_id = data["mails"][0]["_id"]
mail = requests.get(f"{API}/mail", headers=HEADERS,
params={"id": mail_id, "locale": "en"}).json()
print(mail["subject"])
require "net/http"
require "json"
API = "https://api.xitroo.com/v1"
def get(path, params)
uri = URI("#{API}#{path}")
uri.query = URI.encode_www_form(params)
req = Net::HTTP::Get.new(uri)
req["X-Api-Key"] = ENV.fetch("XITROO_API_KEY")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
JSON.parse(res.body)
end
address = "my-test-42@xitroo.com"
since = Time.now.to_i
# ... sign up with address in your app ...
list = {}
until list["mails"]
sleep 2
list = get("/mails", mailAddress: address, mailsPerPage: 10, locale: "en",
minTimestamp: since, maxTimestamp: Time.now.to_i + 60)
end
mail = get("/mail", id: list["mails"][0]["_id"], locale: "en")
puts mail["subject"]
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const api = "https://api.xitroo.com/v1"
func get(path string, params url.Values, out any) error {
req, _ := http.NewRequest("GET", api+path+"?"+params.Encode(), nil)
req.Header.Set("X-Api-Key", os.Getenv("XITROO_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
return json.NewDecoder(res.Body).Decode(out)
}
func main() {
address := "my-test-42@xitroo.com"
since := time.Now().Unix()
// ... sign up with address in your app ...
var list struct {
Mails []struct {
ID string `json:"_id"`
} `json:"mails"`
}
for len(list.Mails) == 0 {
time.Sleep(2 * time.Second)
get("/mails", url.Values{
"mailAddress": {address}, "mailsPerPage": {"10"}, "locale": {"en"},
"minTimestamp": {strconv.FormatInt(since, 10)},
"maxTimestamp": {strconv.FormatInt(time.Now().Unix()+60, 10)},
}, &list)
}
var mail struct {
Subject string `json:"subject"`
}
get("/mail", url.Values{"id": {list.Mails[0].ID}, "locale": {"en"}}, &mail)
fmt.Println(mail.Subject)
}
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class WaitForMail {
static final String API = "https://api.xitroo.com/v1";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String get(String pathAndQuery) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(API + pathAndQuery))
.header("X-Api-Key", System.getenv("XITROO_API_KEY")).build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
String address = URLEncoder.encode("my-test-42@xitroo.com", StandardCharsets.UTF_8);
long since = System.currentTimeMillis() / 1000;
// ... sign up with the address in your app ...
Matcher id;
do {
Thread.sleep(2000);
long until = System.currentTimeMillis() / 1000 + 60;
String list = get("/mails?mailAddress=" + address + "&mailsPerPage=10&locale=en"
+ "&minTimestamp=" + since + "&maxTimestamp=" + until);
id = Pattern.compile("\"_id\":\"([0-9a-f]{24})\"").matcher(list);
} while (!id.find());
// JSON with subject, bodyText, bodyHtml, ... (parse it with your JSON library)
System.out.println(get("/mail?id=" + id.group(1) + "&locale=en"));
}
}
// .NET 6+ (dotnet new console), Program.cs
using System.Net.Http.Json;
using System.Text.Json;
var api = "https://api.xitroo.com/v1";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("XITROO_API_KEY"));
var address = Uri.EscapeDataString("my-test-42@xitroo.com");
var since = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// ... sign up with the address in your app ...
JsonElement mails;
while (true)
{
await Task.Delay(2000);
var until = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var list = await http.GetFromJsonAsync<JsonElement>(
$"{api}/mails?mailAddress={address}&mailsPerPage=10&locale=en&minTimestamp={since}&maxTimestamp={until}");
if (list.TryGetProperty("mails", out mails)) break;
}
var id = mails[0].GetProperty("_id").GetString();
var mail = await http.GetFromJsonAsync<JsonElement>($"{api}/mail?id={id}&locale=en");
Console.WriteLine(mail.GetProperty("subject").GetString());
curl -X POST https://api.xitroo.com/v1/mail/send/premium \
-H "X-Api-Key: $XITROO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "my-test-42@xitroo.com",
"recipient": "you@example.com",
"subject": "Hello",
"bodyText": "Sent with the Xitroo API"
}'
# reply: add "replyMailID": "<_id of a received mail>"
<?php
$ctx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n"
. 'X-Api-Key: ' . getenv('XITROO_API_KEY'),
'content' => json_encode([
'from' => 'my-test-42@xitroo.com',
'recipient' => 'you@example.com',
'subject' => 'Hello',
'bodyText' => 'Sent with the Xitroo API',
// 'replyMailID' => '<_id of a received mail>',
]),
'ignore_errors' => true,
]]);
echo file_get_contents('https://api.xitroo.com/v1/mail/send/premium', false, $ctx), PHP_EOL;
// Node.js 18+, save as send.mjs
const res = await fetch("https://api.xitroo.com/v1/mail/send/premium", {
method: "POST",
headers: {
"X-Api-Key": process.env.XITROO_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "my-test-42@xitroo.com",
recipient: "you@example.com",
subject: "Hello",
bodyText: "Sent with the Xitroo API",
// replyMailID: "<_id of a received mail>",
}),
});
console.log(res.status, await res.json());
import os, requests
r = requests.post(
"https://api.xitroo.com/v1/mail/send/premium",
headers={"X-Api-Key": os.environ["XITROO_API_KEY"]},
json={
"from": "my-test-42@xitroo.com",
"recipient": "you@example.com",
"subject": "Hello",
"bodyText": "Sent with the Xitroo API",
# "replyMailID": "<_id of a received mail>",
},
)
print(r.status_code, r.json())
require "net/http"
require "json"
uri = URI("https://api.xitroo.com/v1/mail/send/premium")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
"X-Api-Key" => ENV.fetch("XITROO_API_KEY"))
req.body = {
from: "my-test-42@xitroo.com",
recipient: "you@example.com",
subject: "Hello",
bodyText: "Sent with the Xitroo API"
# replyMailID: "<_id of a received mail>"
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]string{
"from": "my-test-42@xitroo.com",
"recipient": "you@example.com",
"subject": "Hello",
"bodyText": "Sent with the Xitroo API",
// "replyMailID": "<_id of a received mail>",
})
req, _ := http.NewRequest("POST", "https://api.xitroo.com/v1/mail/send/premium", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", os.Getenv("XITROO_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SendMail {
public static void main(String[] args) throws Exception {
String json = """
{"from": "my-test-42@xitroo.com", "recipient": "you@example.com",
"subject": "Hello", "bodyText": "Sent with the Xitroo API"}""";
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.xitroo.com/v1/mail/send/premium"))
.header("Content-Type", "application/json")
.header("X-Api-Key", System.getenv("XITROO_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());
}
}
// .NET 6+ (dotnet new console), Program.cs
using System.Net.Http.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("XITROO_API_KEY"));
var res = await http.PostAsJsonAsync("https://api.xitroo.com/v1/mail/send/premium", new
{
@from = "my-test-42@xitroo.com",
recipient = "you@example.com",
subject = "Hello",
bodyText = "Sent with the Xitroo API",
// replyMailID = "<_id of a received mail>",
});
Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
GET /v1/mails
| Parameter | Required | Description |
|---|---|---|
mailAddress |
yes | Full address, e.g. my-test-42@xitroo.com |
mailsPerPage |
yes | 1 – 200 |
minTimestamp |
yes | Unix seconds; only mails that arrived after this time |
maxTimestamp |
yes | Unix seconds; only mails that arrived before this time (at most one hour in the future) |
locale |
yes | en or de (language of error messages) |
Response, newest first:
{
"totalMails": 2,
"mails": [
{ "_id": "6748f1c2a1b2c3d4e5f60718", "from": "noreply@example.com",
"subject": "Confirm your account", "arrivalTimestamp": 1793786065.12 }
]
}
An empty mailbox answers with HTTP 200 and {"type": "empty response", ...} instead of a list.
Waiting for a mail: poll with minTimestamp set to the time your test started, so you only see new mails.
GET /v1/mail?id=<_id>&locale=en
Returns deliveredTo, from, subject, arrivalTimestamp, expireTimestamp, bodyText, bodyHtml, rawHeader, messageID and attachments (file names).
GET /v1/mail/attachment?id=<_id>&filename=<name>&locale=en returns the file.
POST /v1/mail/send/premium with header X-Api-Key and a JSON body:
| Field | Required | Description |
|---|---|---|
from |
yes | Your sender address, must end in @xitroo.com, @xitroo.de or @xitroo.fr |
recipient |
yes | Exactly one recipient address |
subject |
yes | Up to 200 characters, no line breaks |
bodyText |
yes | Plain text, up to 100 KB |
replyMailID |
no | _id of a received mail: your mail is sent as a reply to it |
curl -X POST https://api.xitroo.com/v1/mail/send/premium \
-H "X-Api-Key: $XITROO_API_KEY" -H "Content-Type: application/json" \
-d '{"from":"my-test-42@xitroo.com","recipient":"support@example.com","subject":"Hello","bodyText":"Hi there!"}'
{ "sendSuccess": true, "sendsPerMonth": 1000, "sentThisMonth": 17 }
Replies sent to your Xitroo address show up in its mailbox like any other mail. The monthly quota resets on the 1st of each month (UTC); at most 10 mails per minute.
| HTTP | code |
Meaning |
|---|---|---|
| 400 | PARAMETER_INVALID, PARAMETER_REQUIRED |
A parameter is missing or invalid; see field parameter |
| 401 | INVALID_API_KEY |
Key missing, wrong or subscription inactive |
| 429 | RATE_LIMITED |
Too many requests; wait Retry-After seconds |
| 429 | QUOTA_EXCEEDED |
Monthly sending quota used up |
| 502 | SEND_FAILED |
The mail server did not accept the mail; not counted against your quota |
Every error is JSON: {"status": 429, "code": "RATE_LIMITED", "message": "...", "retryAfter": 12}.
Keep your key secret: use it only from servers or CI, never in a public web page or app.
Subscriptions are monthly and billed in US dollars by Stripe; you receive an invoice by e-mail every month. Change plan, update your card, download invoices or cancel anytime in the customer portal (see Manage your subscription above). After cancelling, your key works until the end of the paid period.