ScanMyPassKhatdocs
HomeSign in

Get started

  • Setup
  • DNS records

Reference

  • Sending mail
  • Receiving mail
  • Delivery & webhooks
  • Errors & limits
  • Security

For agents

  • llms.txt

Reference

Sending mail

One endpoint does the sending. Everything else on this page is a field you can add to it.

Authentication#

A bearer token on every request. Your API key is server-side only — it can send as any address on your verified domains.

http
Authorization: Bearer re_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Send a message#

POST/v1/emailsAPI key

Returns 202 as soon as the message is durably queued. It never waits for SMTP.

request
{
  "from": "Acme <hello@example.com>",
  "to": ["customer@gmail.com"],
  "subject": "Your receipt",
  "html": "<p>Thanks for your order.</p>",
  "text": "Thanks for your order."
}
FieldTypeRequiredNotes
fromstringyesuser@domain or Name <user@domain>. The domain must be verified.
tostring | string[]yesUp to 50 recipients across to, cc and bcc combined.
subjectstringyesNo line breaks — they would let a caller inject headers.
htmlstringone ofHTML body.
textstringone ofPlain-text body. Send both — HTML-only mail is filtered harder.
ccstring | string[]no
bccstring | string[]noNever appears in the message headers.
replyTostring | string[]noWhere replies should go, if not the from address.
attachmentsobject[]noUp to 10 files. See below.
headersobjectnoCustom X-… headers. Reserved ones are rejected.
tagsobjectnoString values, shown in the dashboard and useful for filtering.
metadataobjectnoArbitrary JSON, stored and returned. Never sent to the recipient.
scheduledAtstringnoRFC 3339. Omit to send now.

Retrying a send is not safe

This endpoint is not idempotent — a replayed request sends the message again. Retry only when you know nothing was created: a request that got no response at all, a 429, or a 503 EMAIL_QUEUE_FAILED. Do not retry a 500; the message may have been queued before the failure, and a duplicate is worse than an error.

Attachments#

Up to ten files per message, as base64. Three different things people mean by “an image in an email”, and they are not the same mechanism:

What you wantHowShows when images are blocked?
A file the recipient downloadsAn attachment without cidn/a
An image inside the messageAn attachment with cid, referenced <img src="cid:logo">Yes — it is part of the message
An image hosted on your siteA plain <img src="https://…">No

Prefer cid for anything that matters. Most clients block remote images until the reader allows them, so a hotlinked logo shows nothing on first open.

request with attachments
{
  "from": "billing@example.com",
  "to": ["customer@gmail.com"],
  "subject": "Your invoice",
  "html": "<p><img src=\"cid:logo\" width=\"140\" alt=\"Acme\"></p><p>Invoice attached.</p>",
  "text": "Invoice attached.",
  "attachments": [
    { "filename": "invoice.pdf", "content": "JVBERi0xLjQK…", "contentType": "application/pdf" },
    { "filename": "logo.png", "content": "iVBORw0KGgo…", "contentType": "image/png", "cid": "logo" }
  ]
}
FieldRequiredNotes
filenameyesNo path separators.
contentyesBase64 of the raw bytes. No data: prefix.
contentTypenoDefaults to application/octet-stream.
cidnoMakes it an inline part. Must be unique in the message.

There is no url option, on purpose

Some APIs let you pass a URL to fetch. This one does not: fetching a caller-supplied URL from inside our network is server-side request forgery — it would reach the cloud metadata endpoint and our own database. Read the file in your application and send the bytes.

Size

  • Bodies plus decoded attachments must stay under 10 MB. Base64 inflates what you send by a third, but the limit counts real bytes.
  • Receivers cap lower. Gmail rejects above 25 MB and many corporate gateways stop at 10 MB. Large files belong behind a link.

Examples#

cURL
curl -X POST https://mail-api.scanmypass.com/v1/emails \
  -H "Authorization: Bearer $MAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@example.com",
    "to": ["customer@gmail.com"],
    "subject": "Welcome",
    "html": "<h1>Welcome</h1>",
    "text": "Welcome"
  }'
Node — fetch, no dependencies
const response = await fetch("https://mail-api.scanmypass.com/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MAIL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "hello@example.com",
    to: "customer@gmail.com",
    subject: "Welcome",
    html: "<h1>Welcome</h1>",
    text: "Welcome",
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const { id } = await response.json();
Python — requests
import os, requests

r = requests.post(
    "https://mail-api.scanmypass.com/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['MAIL_API_KEY']}"},
    json={
        "from": "hello@example.com",
        "to": ["customer@gmail.com"],
        "subject": "Welcome",
        "html": "<h1>Welcome</h1>",
        "text": "Welcome",
    },
    timeout=30,
)
r.raise_for_status()
email_id = r.json()["id"]
PHP — curl
<?php
$ch = curl_init('https://mail-api.scanmypass.com/v1/emails');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('MAIL_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'from'    => 'hello@example.com',
        'to'      => ['customer@gmail.com'],
        'subject' => 'Welcome',
        'html'    => '<h1>Welcome</h1>',
        'text'    => 'Welcome',
    ]),
]);
$body = curl_exec($ch);

Your logo and footer#

Set once under Branding and added to every message you send, so you do not repeat it in every payload. The footer can be written separately for the HTML and plain-text parts, and {{logo_url}} in the HTML footer expands to your uploaded logo.

NextReceiving mailAccept replies, read them, or have them pushed to you.