Documentation menu
Get started
SendGrid, Mailgun, Postmark and Resend
Already sending through an email API? Keep your code. Point the SDK at MailBench and every message lands in your inbox instead of being delivered.
How it works
MailBench answers the same send requests as SendGrid, Mailgun, Postmark and Resend, with the same success and error shapes, so their SDKs behave as they do against the real service. Change two settings in your staging or CI environment:
- The base URL, from the provider's to MailBench's (below).
- The API key, to your inbox's SMTP username and password joined by a colon:
your-inbox-user:your-inbox-pass. Copy them from the inbox in the dashboard.
Nothing is delivered. Each message counts toward your monthly quota exactly like one sent over SMTP, and the same size, attachment and rate limits apply.
SendGrid
Base URL https://app.mailbench.dev/sendgrid instead of api.sendgrid.com. Supported: POST /v3/mail/send.
SENDGRID_API_KEY=SG.your-inbox-user:your-inbox-passThe SG. prefix is optional; it stops the Node SDK warning about the key format.
import sgMail from "@sendgrid/mail"
import client from "@sendgrid/client"
sgMail.setClient(client)
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
// After setApiKey, which resets the base URL to SendGrid's:
client.setDefaultRequest("baseUrl", "https://app.mailbench.dev/sendgrid/")
await sgMail.send({ to: "ann@example.com", from: "app@example.com", subject: "Welcome", text: "Hi Ann" })curl https://app.mailbench.dev/sendgrid/v3/mail/send \
-H "Authorization: Bearer $SENDGRID_API_KEY" \
-H "Content-Type: application/json" \
-d '{"personalizations":[{"to":[{"email":"ann@example.com"}]}],
"from":{"email":"app@example.com"},"subject":"Welcome",
"content":[{"type":"text/plain","value":"Hi Ann"}]}'- Each personalization becomes its own message, with its subject, headers and substitutions applied.
- A dynamic template (template_id) is stored as a note listing the template ID and its dynamic_template_data, since MailBench cannot render SendGrid-hosted templates.
- Python: SendGridAPIClient(api_key, host="https://app.mailbench.dev/sendgrid"). PHP: new \SendGrid($key, ["host" => "https://app.mailbench.dev/sendgrid"]).
Mailgun
Base URL https://app.mailbench.dev/mailgun instead of api.mailgun.net. Supported: POST /v3/{domain}/messages, POST /v3/{domain}/messages.mime.
MAILGUN_API_KEY=your-inbox-user:your-inbox-passSent as Basic auth with the user "api", as the SDKs do. Any domain works.
import Mailgun from "mailgun.js"
import FormData from "form-data"
const mg = new Mailgun(FormData).client({
username: "api",
key: process.env.MAILGUN_API_KEY,
url: "https://app.mailbench.dev/mailgun",
})
await mg.messages.create("mg.example.com", { from: "app@example.com", to: ["ann@example.com"], subject: "Welcome", text: "Hi Ann" })curl https://app.mailbench.dev/mailgun/v3/mg.example.com/messages \
-u "api:$MAILGUN_API_KEY" \
-F from="app@example.com" -F to="ann@example.com" \
-F subject="Welcome" -F text="Hi Ann"- Attachments and inline files, h: headers, o:tag and v: variables are kept on the captured message.
- A stored template is saved as a note with its t:variables.
- PHP: Mailgun::create($key, "https://app.mailbench.dev/mailgun").
Postmark
Base URL https://app.mailbench.dev/postmark instead of api.postmarkapp.com. Supported: POST /email, POST /email/batch, POST /email/withTemplate.
POSTMARK_SERVER_TOKEN=your-inbox-user:your-inbox-passSent in the X-Postmark-Server-Token header.
import * as postmark from "postmark"
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN, {
requestHost: "app.mailbench.dev/postmark",
})
await client.sendEmail({ From: "app@example.com", To: "ann@example.com", Subject: "Welcome", TextBody: "Hi Ann" })curl https://app.mailbench.dev/postmark/email \
-H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"From":"app@example.com","To":"ann@example.com","Subject":"Welcome","TextBody":"Hi Ann"}'- A batch answers one result per message, failures included, as Postmark does.
- Tag, MessageStream and Metadata are kept as X-PM- headers on the captured message.
- A template send is stored as a note with the TemplateModel.
Resend
Base URL https://app.mailbench.dev/resend instead of api.resend.com. Supported: POST /emails, POST /emails/batch.
RESEND_API_KEY=your-inbox-user:your-inbox-pass// The Node SDK reads RESEND_BASE_URL:
// RESEND_BASE_URL=https://app.mailbench.dev/resend
import { Resend } from "resend"
const resend = new Resend(process.env.RESEND_API_KEY)
await resend.emails.send({ from: "app@example.com", to: ["ann@example.com"], subject: "Welcome", html: "<p>Hi Ann</p>" })curl https://app.mailbench.dev/resend/emails \
-H "Authorization: Bearer $RESEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"app@example.com","to":"ann@example.com","subject":"Welcome","html":"<p>Hi Ann</p>"}'- A batch is checked as a whole before anything is stored, as Resend does.
- Attachments must carry their content. Attachments by path (a URL) are refused: MailBench never fetches a URL you send it.
Limits and errors
A message over the size limit is refused with 413; more than about ten messages a second per inbox, or a full monthly quota, with 429. Repeated wrong keys lock out the sending IP for a while, as for SMTP logins. Batches take up to 50 messages, and each message up to 100 recipients. Errors use the provider's own format, so your SDK reports them as usual.

