NAV Navbar

API Docs

testmail.app/docs

Get Started

Welcome aboard! Here you'll find all the documentation you'll need. We're going to assume that you have already signed up and you have access to your console (to retrieve your namespaces and API keys).

Essential concepts

testmail.app receives emails at {namespace}.{tag}@inbox.testmail.app where {namespace} is a unique namespace assigned to you (see your console) and {tag} can be anything you choose.

Namespaces

Think of your namespace as a collection of mailboxes - each namespace supports an unlimited number of email addresses.

Here's an example: suppose your namespace is acmeinc; if you send a test email to [email protected] and another test email to [email protected], both emails will be received in the acmeinc namespace - the first will have the tag hello and the second will have the tag hey.

Tags

The tag can be anything you choose. By using different tags, you can create new email addresses and mailboxes on the fly!

Here's an example: suppose you want to simulate new user signups for testing your app; you can create new users named John with the email [email protected] and Albert with the email [email protected], and then retrieve emails sent to John and Albert separately by using the tag filter or together by querying the whole acmeinc namespace.

API keys

API keys (retrieve yours from the console) are required to authenticate your API requests. API keys are authorized to access one or more namespaces - you can configure API key permissions in your console.

Retention

Each namespace has a default retention period (1, 3, or 30 days depending on your plan); after this period, old emails are automatically deleted. You don't need to manually delete emails.

If your use case requires a longer (or shorter) retention period, please reach out - we can customize your namespace retention.

Choosing an API

testmail.app offers two APIs: a simple JSON API and a full-featured GraphQL API. The simple JSON API is easy to get started but has some limitations (see the comparison table).

If you're already familiar with GraphQL, you can get started right away by taking a look at the schema and docs popovers on the right in the GraphQL playground - it's fully self-documenting! You can build and test queries in the GraphQL playground or in any API testing tool of your choice (like Postman or Insomnia). Remember to include auth headers when using the GraphQL API.

Comparing the APIs

(Feature)
(Description)
Simple JSON API GraphQL API
Basic filters
Namespace, tag, tag_prefix, and timestamp range
Yes Yes
Pagination
Using the limit and offset parameters
Yes Yes
Spam reports
Query spam_score and spam_report
Yes Yes
"Live" queries
Waiting for new emails
Yes Yes
Return select fields
(Instead of all fields)
No Yes
Advanced filters
Include/exclude by from, subject, text/html body, etc.
No Yes
Custom sort
Default sort is descending by timestamp
No Yes

JSON API: Guide

Quickstart

# Get your apikey and namespace from the console after signup/signin
APIKEY="YOUR_APIKEY"
NAMESPACE="YOUR_NAMESPACE"

# Use any HTTP client (or your browser)
curl "https://api.testmail.app/api/json?apikey=${APIKEY}&namespace=${NAMESPACE}&pretty=true"

Expect something like this (example):

{
  "result": "success",
  "message": null,
  "count": 0,
  "limit": 10,
  "offset": 0,
  "emails": []
}

The JSON API is designed to be simple and work everywhere (including your browser). You can use any HTTP client in any language or environment of choice - just make HTTP GET requests.

The API endpoint: https://api.testmail.app/api/json

Just two parameters are mandatory: your API key (&apikey=YOUR_APIKEY) and your namespace (&namespace=YOUR_NAMESPACE). If you're viewing the output in your browser, add &pretty=true to prettify the output (makes it more readable).

The API returns a result object (see the example on the right) with the following structure:

To explore all API features and options, check out the JSON API reference.

Waiting for new email

When you're expecting to receive new emails, instead of repeatedly querying the inbox, add the &livequery=true parameter to your API call. "Live" queries wait until at least one email is matched before returning. Here's how it works:

Deleting emails

You might feel the need to delete emails in your namespace after each test - this is anti-pattern!

Instead, when you need to filter out emails from previous tests, we recommend recording the start time at the beginning of your test and feeding this to the timestamp_from filter. Or, when you need to run multiple tests simultaneously, we recommend prefixing your tags with a unique test ID and feeding this to the tag_prefix filter.

Emails are automatically deleted after the default retention period for your namespace (1, 3, or 30 days depending on your plan). We don't offer a delete function in our APIs or console. This is because we index and store emails using immutable data structures that prioritize query performance (so that you can retrieve emails quickly) with the tradeoff that deleting emails is a somewhat expensive process (computationally) that we do via scheduled cron jobs every night.

JSON API: Reference

?apikey={string}

Your API key - mandatory for all API requests.

Syntax: &apikey=1c1568a3-db0a-4189-bf28-7987f14f19d0
(example only - not a real apikey)
Required: yes

&namespace={string}

The namespace you wish to query - mandatory for all API requests.

Syntax: &namespace=abc123
(example only - use your namespace)
Required: yes

&pretty=true

Whether to prettify JSON output (makes it more readable).

Use this when you're testing the API in your browser; keep it off when using the API programmatically (in production).

Syntax: "true" or "false" (without the quotes)
Required: no - it's optional
Default: false

&headers=true

Headers example in the email object:

{
  "headers": [
    {
      "line": "Message-Id: <...>",
      "key": "message-id"
    },
    {
      "line": "Mime-Version: 1.0",
      "key": "mime-version"
    }
    // ...etc.
  ]
  // ...rest of the email
}

Whether to include email headers in each email object.

Syntax: "true" or "false" (without the quotes)
Required: no - it's optional
Default: false

&spam_report=true

Spam report example in the email object:

{
  "spam_score": 1.158,
  "spam_report": "Spam detection software, running on the system \"inbox.testmail.app\", has\nidentified this incoming email as possible spam.  The original message\nhas been attached to this so you can view it (if it isn't spam) or label\nsimilar future email.  If you have any questions, see\n@@[email protected]@ for details.\n\nContent preview:  Hello there! Here is a test html message. hello world! [...]\n   \n\nContent analysis details:   (1.2 points, 5.0 required)\n\n pts rule name              description\n---- ---------------------- --------------------------------------------------\n 0.8 HTML_IMAGE_RATIO_02    BODY: HTML has a low ratio of text to image area\n 0.0 HTML_MESSAGE           BODY: HTML included in message\n 0.3 HTML_IMAGE_ONLY_04     BODY: HTML: images with 0-400 bytes of words\n 0.0 T_MIME_NO_TEXT         No text body parts\n\n"
  // ...rest of the email
}

Whether to include spam reports in each email object. This will add a spam_score field (floating point number that can be negative) and a spam_report field. See the spam testing guide for details.

Syntax: "true" or "false" (without the quotes)
Required: no - it's optional
Default: false

&tag={string}

Filter emails by tag (exact match).

Example: an email sent to [email protected] will be in the test namespace and have the john.smith tag.

Syntax: &tag=test
(example only)
Required: no - it's optional

&tag_prefix={string}

Filter emails by tag prefix.

Example: a query filtered by &tag_prefix=john will match [email protected] and also match [email protected] because both tags (john.smith and john.tory) start with "john".

Syntax: &tag_prefix=test
(example only)
Required: no - it's optional

&timestamp_from={int}

Filter emails by starting unix timestamp in milliseconds (number of milliseconds since 1st January 1970, UTC)

Syntax: integer (e.g. &timestamp_from=1579916290055)
Required: no - it's optional

&timestamp_to={int}

Filter emails by ending unix timestamp in milliseconds (number of milliseconds since 1st January 1970, UTC)

Syntax: integer (e.g. &timestamp_to=1579916298055)
Required: no - it's optional

&limit={int}

Maximum number of emails to return (useful for pagination when used with the offset parameter).

Syntax: integer (e.g. &limit=5)
Required: no - it's optional
Default: 10

&offset={int}

Number of emails to skip/ignore (useful for pagination when used with the limit parameter).

Syntax: integer (e.g. &offset=5)
Required: no - it's optional
Default: 0

&livequery=true

Whether to wait for new emails before responding. See waiting for new email for an explanation of how this works.

Syntax: "true" or "false" (without the quotes)
Required: no - it's optional
Default: false

GraphQL API: Guide

Setup and auth

const GraphQLClient = require('@testmail.app/graphql-request').GraphQLClient;
const testmailClient = new GraphQLClient(
  // API endpoint:
  'https://api.testmail.app/api/graphql',
  // Use your API key:
  { headers: { 'Authorization': 'Bearer API_KEY' } }
);

To use the GraphQL API, you need a GraphQL (recommended) or HTTP client. We've included a Javascript example on the right using a clone of the popular graphql-request client with added features like built-in retries: @testmail.app/graphql-request.

The API endpoint: https://api.testmail.app/api/graphql

Access to the API is secured using API keys. You can get your API key or register a new API key in the developer console.

The API key must be included in all API requests to the server in an authorization header that looks like this:

Authorization: Bearer API_KEY

Querying the inbox

testmailClient.request(`{
  inbox (
    namespace:"YOUR_NAMESPACE"
  ) {
    result
    message
    count
  }
}`).then((data) => {
  console.log(data.inbox);
});

Expect something like this (example):

{
  "result": "success",
  "message": null,
  "count": 2
}

There's just one query you need: the inbox query.

In the example on the right (a very simple query), testmail will return a count for the total number of emails in the specified namespace. It specifies one argument - the namespace (always mandatory) - and asks for the result, message, and count.

Successful queries will return result: 'success'; unsuccessful queries will return result: 'fail'. In case of failure, the message will usually provide an explanation.

The count will return an integer. Of course: you can query for a lot more than the count. Please see the docs tab in the GraphQL playground for a full API reference.

If you're expecting the query to return many emails and you need pagination, you can use the limit (default is 10) and offset arguments.

"Live" queries

const timestamp = Date.now();
testmailClient.request(`{
  inbox (
    namespace:"YOUR_NAMESPACE"
    tag:"john.smith"
    timestamp_from:${timestamp}
    livequery:true
  ) {
    result
    message
    emails {
      from
      from_parsed {
        address
        name
      }
      subject
    }
  }
}`).then((data) => {
  console.log(data.inbox);
});

Expect something like this (example):

{
  "result": "success",
  "message": null,
  "emails": [{
    "from": "testmail app <[email protected]>",
    "from_parsed": {
      "address": "[email protected]",
      "name": "testmail app"
    },
    "subject": "Please confirm your email"
  }]
}

When you're expecting to receive an email, instead of repeatedly querying the inbox, use a "live" query.

"Live" queries wait until at least one email is matched before returning. In the example on the right, the testmail API will wait until a new email addressed to [email protected] is received before returning.

"Live" queries are implemented using HTTP 307 redirects. Here's how it works:

"Live" queries can return multiple emails. This happens either because there are already multiple emails matching the query (so the "live" query returns immediately) or because multiple new emails were received and processed at exactly the same time (this is rare).

Sorting and filtering

testmailClient.request(`{
  inbox (
    namespace:"YOUR_NAMESPACE"
    tag_prefix:"peter"
    advanced_filters:[{
      field:subject
      match:exact
      action:include
      value:"Please confirm your email"
    }]
    advanced_sorts:[{
      field:tag,
      order:asc
    }, {
      field:timestamp,
      order:desc
    }]
  ) {
    result
    message
    count
    emails {
      tag
      timestamp
    }
  }
}`).then((data) => {
  console.log(data.inbox);
});

Expect something like this (example):

{
  "result": "success",
  "message": null,
  "count": 4,
  "emails": [{
    "tag": "peter.pan",
    "timestamp": 1565139749955
  }, {
    "tag": "peter.pan",
    "timestamp": 1565136514957
  }, {
    "tag": "peter.parker",
    "timestamp": 1565140065791
  }, {
    "tag": "peter.parker",
    "timestamp": 1565140041284
  }]
}

The GraphQL API has excellent options for sorting and filtering emails. A couple of typical use cases:

If you don't use sufficiently specific filters, the live query will return any new email that hits the inbox - not necessarily the email you're expecting. This is especially important when you have many back-to-back email tests (you could receive emails in an unexpected order) or when you have parallel builds in your CI pipelines (you could receive emails from another test).

If you want to check whether an email with <h1>Getting started</h1> in the html body just hit the inbox, you can use the wildcard filter on the html field and query the count of matched emails instead of downloading the full html content and searching for that string. However, note that if the html field is greater than 30kb, it might not be indexed for search! If you need full text search over large emails, please contact us.

Note that the timestamp_from, timestamp_to, tag, and tag_prefix options are the most performant options for filtering emails.

Tags are indexed using n-grams - so tags and subsets of tags can be queried incredibly efficiently. Here is a typical usage pattern that takes advantage of this:

Send emails to: [email protected] (where the tag is category.subcategory), and then query for emails using the tag_prefix:

Nesting tags like this allows you to group/categorize inboxes, reduce conflicts, and use fewer advanced_filters (which are less performant).

Emails are sorted by the timestamp field in descending order by default (newest emails first). You can change this using advanced_sorts; you can also use multiple sorts (see the example on the right).

GraphQL API: Reference

The complete GraphQL API reference is available in the GraphQL playground (see the schema and docs popovers on the right).

Spam Testing Guide

Spam filters have become fairly good at differentiating "spam" (bad) from "ham" (good): most modern spam detection tools incorporate machine learning and sophisticated filters that go beyond simple rules like whether the subject line includes "Congratulations!!!" or whether the body mentions a "Nigerian prince" who wants to send money for "safekeeping".

Generally you don't have to worry about spam filters if:

To help you improve deliverability, we spam-test every email and attach spam scores and detailed spam reports that you can query via API.

How it works

We use SpamAssassin (the most popular open-source anti-spam platform) to spam-test emails.

Spam scores and reports generated by SpamAssassin are not "universal" - they are highly dependent on the enabled rule sets and plugins. Each mailbox provider configures their installation differently: some build their own rule sets, some set higher or lower thresholds for classifying spam, etc.

Our configuration includes the most commonly enabled rule sets and uses the default spam threshold of 5 points. We add and update rules from time to time - so spam scores can change over time for the same email.

We recommend using these spam reports as guidelines: a high score suggests a high probability of being marked as spam - but a low score does not guarantee inbox placement.

TL;DR

Examples

See the following examples of common end-to-end tests using cypress (a JavaScript end-to-end testing framework).

Signup (JSON API)

/**
 * Required environment variables: TESTMAIL_APIKEY and TESTMAIL_NAMESPACE
 */

// Use any HTTP client
const axios = require('axios').default;

// Setup our JSON API endpoint
const ENDPOINT = `https://api.testmail.app/api/json?apikey=${process.env.TESTMAIL_APIKEY}&namespace=${process.env.TESTMAIL_NAMESPACE}`;

// Randomly generating the tag...
const ChanceJS = require('chance');
const chance = new ChanceJS();
const TAG = chance.string({
  length: 12,
  pool: 'abcdefghijklmnopqrstuvwxyz0123456789'
});

// This is our email address (for this test)
const TESTEMAIL = `${process.env.TESTMAIL_NAMESPACE}.${TAG}@inbox.testmail.app`;

// (Optional) Record the start time for the timestamp_from filter
const startTimestamp = Date.now();

// Use any test environment (ours is cypress)
context('Some testing suite', () => {
  context('Signup', () => {
    before(() => {
      // Your test signup page:
      cy.visit('http://localhost:8080/signup');
    });
    it('enter email', () => {
        cy.get('#emailinput')
          .type(TESTEMAIL);
      });
    it('click signup', () => {
      cy.get('#signupbutton')
        .click();
    });
  });
  context('Verify email', () => {
    let inbox;
    before((done) => {
      // Configure timeout
      this.timeout(1000*60*5); // five minutes
      // Query the inbox
      axios.get(`${ENDPOINT}&tag=${TAG}&timestamp_from=${startTimestamp}&livequery=true`).then((response) => {
        inbox = response.data;
        done();
      }).catch((err) => {
        done(err);
      });
    });
    it('Should work', () => {
      expect(inbox.result).to.equal('success');
    });
    it('There should be one email in the inbox', () => {
      expect(inbox.count).to.equal(1);
    });
    it('Get the email verification link', () => {
      // Extract the verification link
      expect(inbox.emails[0].html).to.have.string('id="verifyemail"');
      const extract = /id="verifyemail">(.+?)<\//gi;
      const link = extract.exec(inbox.emails[0].html)[1];
      // Tada! Now we can proceed to test the link, etc.
    });
  });
});

See the example on the right of an end-to-end test for new user signups via JSON API. The same example via GraphQL API is available here.

Notes:

Signup (GraphQL API)

/**
 * Required environment variables: TESTMAIL_APIKEY and TESTMAIL_NAMESPACE
 */

// Use any GraphQL client (or HTTP client - but let's keep this simple)
const GraphQLClient = require('@testmail.app/graphql-request').GraphQLClient;
const testmailClient = new GraphQLClient(
  // API endpoint:
  'https://api.testmail.app/api/graphql',
  // Use your API key:
  { headers: { 'Authorization': `Bearer ${process.env.TESTMAIL_APIKEY}` } }
);

// Randomly generating the tag...
const ChanceJS = require('chance');
const chance = new ChanceJS();
const TAG = chance.string({
  length: 12,
  pool: 'abcdefghijklmnopqrstuvwxyz0123456789'
});

// This is our email address (for this test)
const TESTEMAIL = `${process.env.TESTMAIL_NAMESPACE}.${TAG}@inbox.testmail.app`;

// (Optional) Record the start time for the timestamp_from filter
const startTimestamp = Date.now();

// Use any test environment (ours is cypress)
context('Some testing suite', () => {
  context('Signup', () => {
    before(() => {
      // Your test signup page:
      cy.visit('http://localhost:8080/signup');
    });
    it('enter email', () => {
        cy.get('#emailinput')
          .type(TESTEMAIL);
      });
    it('click signup', () => {
      cy.get('#signupbutton')
        .click();
    });
  });
  context('Verify email', () => {
    let inbox;
    before((done) => {
      // Configure timeout
      this.timeout(1000*60*5); // five minutes
      // Query the inbox
      testmailClient.request(`{
        inbox (
          namespace:"${process.env.TESTMAIL_NAMESPACE}"
          tag:"${TAG}"
          timestamp_from:${startTimestamp}
          livequery:true
        ) {
          result
          count
          emails {
            subject
            html
            text
          }
        }
      }`).then((data) => {
        inbox = data.inbox;
        done();
      }).catch((err) => {
        done(err);
      });
    });
    it('Should work', () => {
      expect(inbox.result).to.equal('success');
    });
    it('There should be one email in the inbox', () => {
      expect(inbox.count).to.equal(1);
    });
    it('Get the email verification link', () => {
      // Extract the verification link
      expect(inbox.emails[0].html).to.have.string('id="verifyemail"');
      const extract = /id="verifyemail">(.+?)<\//gi;
      const link = extract.exec(inbox.emails[0].html)[1];
      // Tada! Now we can proceed to test the link, etc.
    });
  });
});

See the example on the right of an end-to-end test for new user signups via GraphQL API. The same example via JSON API is available here.

Notes:

Troubleshooting

Power users may occasionally encounter the issues described below (they should be extremely rare for most users).

Timeouts (307 loops)

Timeouts during live queries (in the form of 307 redirects every minute forever) are the most common issue. There are several reasons for this:

Of course: this is what we are testing in the first place! If your app is not sending the right emails or fails to send emails at all, the tests will naturally timeout or fail (as they should).

While the vast majority of emails are received within a few seconds, some emails can take 5 minutes or more, and on rare occasions emails can take days to deliver! When emails fail to deliver (e.g. because of a connection issue), the sending server usually makes a certain number of retry attempts with an increasing time interval between retries. So when you send many emails, and when you've integrated testmail in your CI/CD pipeline, you will inevitably encounter edge cases.

To minimize build timeouts while waiting to receive emails, make sure the timeout configured in your test environment is sufficiently high. We use 5 minutes in our pipeline, but we can't recommend this number for everyone. Here's the tradeoff: if the timeout is too low, you'll get more build failures (you'll just have to retry/restart these builds). If the timeout is too high, the edge cases can result in builds that take very long and clog your pipeline.

If your mail server IP address or ISP has a bad reputation, spam filters can reject or delay your emails. Misconfigurations on the sending server can cause delays, deliverability problems, and failures. One key benefit of end-to-end testing is discovering these problems in advance - before your users experience them.

Here's a real example: we signed up for Mailgun and started testing email sends. We noticed that many of our emails were not reaching the inbox. This was because, as a new customer, Mailgun had put us in a shared pool of IPs with a poor spam reputation. After reaching out to their support team and verifying our business (the types of emails we send, email list management policies, privacy policy, etc.), they upgraded us to an IP pool with a better reputation. This is not an endorsement or criticism of Mailgun; every good email provider has to do this (in some way) to prevent spammers from signing up and sending bulk emails from their quality IPs.

Rate limiting (429 errors)

We use API rate limits to prevent abuse and survive common developer mistakes (like infinite loops) that we are exposed to as a developer tool with public APIs.

We have two layers of rate limits that mitigate different types of stresses on our system:

Global API rate limits

You may trigger global rate limits if you sustain >3 requests/second for many minutes from the same IP. Global rate limits apply to all requests from the same IP across all APIs. Please reach out if your use case requires higher global rate limits - we can whitelist select IPs. Note that we may adjust global rate limits occasionally to mitigate DDoS attacks and other malicious activity.

API key rate limits

On paid plans you may trigger the API key rate limit if you exceed:

We can increase this limit for your API keys if your use case requires it - just let us know.

Individual API keys that breach this limit are temporarily blacklisted for up to an hour. We implemented this limit to survive edge cases like infinite loops inside matrix builds in CI/CD pipelines where the loop is parallelized across many machines with different IPs - creating DDoS monsters!

On free plans you may trigger API key rate limits if you exceed:

We implemented these free plan limits to prevent anti-pattern use that consumes excessive resources (e.g. querying the inbox for new emails every second). Most free users never exceed these limits. If you breach them, please upgrade to any paid plan.

5xx status codes

5xx class errors (500, 502, 503, 504) should be extremely rare: we go to great lengths to maintain high availability and uptime. However, a small number of these are inevitable in any environment at scale.

To avoid these edge cases, we recommend configuring automatic retries on your client. Our @testmail.app/graphql-request client includes automatic retries with exponential backoff by default.

Performance

You can expect excellent performance when searching, retrieving, and filtering emails by namespace, tag, and timestamp. However, using advanced_filters on other fields is generally less performant - but you're unlikely to notice this until you're working with very large result sets.

The GraphQL API includes features like wildcard search on the text/html email body. These features have some limits:

Consistency

In very rare cases, two identical API calls may return different results. This is an inevitable tradeoff from prioritizing high availability in distributed systems (see CAP theorem). We have not seen any use cases for testmail.app where this might be a problem - if you can think of one, let us know :)

Get Help

If you cannot find answers, please reach out - we are happy to help. You can reach us via email or live chat. Pro and enterprise customers should use their registered emails or signin for priority support.

Support requests are handled by software engineers who actively build and maintain testmail.app

We typically respond within a few hours. Priority support requests are often answered within minutes.