API mocks

On this page

Mandrill-compatible email API

A mock of the Mandrill (Mailchimp Transactional) API. Your app sends transactional email with the official SDK or plain HTTP, exactly as it would through Mandrill, and every accepted message lands in one of your team's Testlie email inboxes as a real email, with the HTML preview, link and accessibility checks, the spam report and everything else the email sandbox does. Nothing is delivered to real people.

Testlie is not affiliated with, endorsed or sponsored by Mailchimp or Intuit. "Mandrill" and "Mailchimp" are used only to describe API compatibility.

Connect

Create a Mandrill-compatible mock under API mocks → Push & messaging. Use its base URL in place of https://mandrillapp.com and its API key:

Credential Use
API key The key field of every call's JSON body, like Mandrill
Webhook key Signs webhook batches (X-Mandrill-Signature); webhooks/list returns it as auth_key

Calls are POST {base URL}/api/1.0/<group>/<method>.json with a JSON body. The official SDKs use /api/1.3; any 1.x version and the .json suffix are accepted.

js
import mailchimpFactory from "@mailchimp/mailchimp_transactional";
const mailchimp = mailchimpFactory(process.env.MANDRILL_API_KEY);
mailchimp.basePath = "https://testlie.com/mock/<id>/api/1.3";
await mailchimp.messages.send({ message: { from_email: "app@example.com", subject: "Hi", text: "Hello", to: [{ email: "jane@example.com" }] } });

In Python, set client.host = "https://testlie.com/mock/<id>/api/1.3" on mailchimp_transactional.Client.

A wrong or missing key answers HTTP 500 with Mandrill's error body:

json
{ "status": "error", "code": -1, "name": "Invalid_Key", "message": "Invalid API key" }

Other errors use the same shape: ValidationError (code -2), Unknown_Template (5), Invalid_Template (6), Unknown_Message (11), Unknown_Webhook (3), Unknown_Method, PaymentRequired (the team's monthly email quota is used up) and Too_Many_Requests (HTTP 429).

Where messages go

Each accepted message is captured in the mock's capture inbox (Settings tab). By default that is the oldest inbox of the mock's project, or else the team's oldest inbox; sends fail with GeneralError while the team has no inbox. Only your own team's inboxes can be chosen.

Captured messages count against the plan's monthly email messages and the inbox's per-minute rate, like mail sent over SMTP. Inbox webhooks, forwarding rules and automatic spam checks apply as usual.

Like Mandrill, every recipient gets their own copy with only their address in To (preserve_recipients: false, the default). With preserve_recipients: true one message shows every to and cc recipient; bcc recipients are on the envelope only. Each message has Message-ID: <_id@mandrill.testlie.invalid> and an X-Mandrill-Message-Id header with Mandrill's _id, so tests can match the API response to the email.

Supported calls

Call Notes
users/ping, users/ping2, users/info, users/senders "PONG!", { "PING": "PONG!" }
messages/send html, text, subject, from_email, from_name, to[] (to/cc/bcc), headers, attachments, images (inline, by cid), tags, metadata, merge_vars, global_merge_vars, merge, merge_language, preserve_recipients, bcc_address, async. Up to 100 recipients
messages/send-template template_name, template_content (fills mc:edit regions), plus the message fields above
messages/info, messages/content By _id
messages/search query, date_from, date_to, tags, senders, limit
senders/list Per sender address: sent, bounces, rejects, opens, clicks
templates/add, info, update, publish, delete, list, render A per-mock template store (up to 200 templates). send-template uses the published version
webhooks/list, add, info, delete Webhooks added through the API receive the events they ask for; the mock's own webhook URL (Settings) gets all events

The response of a send is Mandrill's array:

json
[{ "email": "jane@example.com", "status": "sent", "reject_reason": null, "_id": "4f0a…" }]

status is sent, queued (with async: true), rejected (with reject_reason) or invalid (not an email address).

Merge tags: *|NAME|* (Mailchimp, the default) or {{name}} / {{{name}}} with merge_language: "handlebars". Per-recipient merge_vars override global_merge_vars. Unknown tags are left in place so a missing merge var is visible in the captured email. Handlebars helpers ({{#if}}, {{#each}}) are not evaluated.

Search: whitespace-separated terms that must all match. email:, subject:, sender: (or from_email:), tags:, state: and template: look at one field; a bare term looks at the email, subject and sender. * is a wildcard; * alone matches everything. This is a small subset of Mandrill's search syntax.

Not implemented: send_at scheduling (messages are captured at once), messages/send-raw, inbound routes, subaccounts, IP pools, exports and URL tracking reports.

Simulated outcomes

The recipient's address decides what happens: the local part itself or one of its +tags, e.g. bounce@example.com or jane+bounce@example.com.

Address tag Response Captured Events
(none) sent yes send
reject rejected, hard-bounce no reject
spam rejected, spam no reject
unsub rejected, unsub no reject
bounce sent yes send, then hard_bounce
softbounce sent yes send, then soft_bounce
defer sent yes deferral, then send
open sent yes send, then open
click sent yes send, then open and click (on the first link of the HTML)

Later events come after the mock's asynchronous steps delay (Scenarios tab, 2 seconds by default), and messages/info reflects them from then on (state: "bounced", opens, clicks, smtp_events). The Outcome: decline scenario rejects every recipient with the chosen reason.

Webhooks

Set a webhook URL on the mock (or add one with webhooks/add). Events arrive in batches, as Mandrill sends them: a form-encoded POST whose mandrill_events field is a JSON array of events (send, deferral, hard_bounce, soft_bounce, open, click, reject, ...), each with event, _id, ts and msg.

Every batch carries X-Mandrill-Signature, computed like Mandrill documents it: start with the webhook URL exactly as registered, append each POST field's name and value with the names sorted, then base64-encode the HMAC-SHA1 of that string with the webhook key.

js
import { createHmac } from "node:crypto";
function verify(url, params, signature, key) {
  let data = url;
  for (const k of Object.keys(params).sort()) data += k + params[k];
  return createHmac("sha1", key).update(data).digest("base64") === signature;
}

The webhook scenarios (delay, drop, duplicate, bad signature) apply, deliveries are retried, and each one is shown on the mock's Webhooks tab with a replay button.