Xitroo API

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.

How to get your API key

1

Choose a plan

Pick Developer, Team or Business below and click Subscribe.

2

Pay securely with Stripe

Card or other payment methods. Monthly in US dollars, invoice by e-mail, cancel anytime.

3

Receive your key

Your key (xit_…) appears right after payment and is also sent to your e-mail address.

Lost your key? Request a new one →

Plans

plus applicable sales tax / VAT, billed monthly in US dollars, cancel anytime

Manage your subscription

Change plan, update payment details, download invoices or cancel. Enter your API key.

Lost your API key?

Enter the e-mail address of your subscription. We send you a new key; the old one stops working.

Quick start

  1. Get your API key (see above): it is shown right after payment and sent to you by e-mail. It starts with xit_.
  2. Send it in the header X-Api-Key with every request.
  3. Any address @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.

Examples

Tested code in eight languages. Set the environment variable XITROO_API_KEY to your key.

Wait for a confirmation mail

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());

Send an e-mail

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()}");

Receive e-mails

List a mailbox

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.

Read one e-mail

GET /v1/mail?id=<_id>&locale=en

Returns deliveredTo, from, subject, arrivalTimestamp, expireTimestamp, bodyText, bodyHtml, rawHeader, messageID and attachments (file names).

Attachments

GET /v1/mail/attachment?id=<_id>&filename=<name>&locale=en returns the file.

Send e-mails

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.

Limits and errors

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.

Billing

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.