MailLandDocs

REST API

Send emails using the Mail.Land REST API with full request and response reference

Send transactional emails via POST /v1/transactional/send. This is the primary way to send email through MailLand.

Authentication

Include your API key in the X-API-Key header:

X-API-Key: ml_pet_Ab3Kx9mN2pQ7rT4wL8yD...

The credential must have the Api channel enabled and be a Sending product credential.

Send a Single Email

POST https://api.mail.land/v1/transactional/send
Send an email
curl -X POST https://api.mail.land/v1/transactional/send \
  -H "X-API-Key: ml_pet_Ab3Kx9mN2pQ7rT4wL8yD..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": {"email": "[email protected]"},
    "to": [{"email": "[email protected]"}],
    "subject": "Hello from MailLand",
    "htmlContent": "<p>Your email content here</p>"
  }'

Request Body

Full request example
{
  "from": {
    "email": "[email protected]",
    "name": "Your App"
  },
  "to": [
    {
      "email": "[email protected]",
      "name": "Jane Doe",
      "variables": {
        "firstName": "Jane",
        "orderNumber": "ORD-12345"
      }
    }
  ],
  "subject": "Your order #{{orderNumber}} has shipped",
  "htmlContent": "<h1>Hi {{firstName}},</h1><p>Your order #{{orderNumber}} is on its way.</p>",
  "textContent": "Hi {{firstName}}, Your order #{{orderNumber}} is on its way.",
  "replyTo": "[email protected]",
  "tags": ["order-shipped", "transactional"],
  "customHeaders": {
    "X-Entity-Ref-ID": "order-12345"
  },
  "tracking": {
    "opens": true,
    "clicks": true
  },
  "priority": "High",
  "idempotencyKey": "order-12345-shipped-v1"
}

Field Reference

FieldTypeRequiredDescription
fromobjectYesSender address
from.emailstringYesSender email (must be on a verified domain)
from.namestringNoDisplay name
toarrayYesRecipient list
to[].emailstringYesRecipient email address
to[].namestringNoRecipient display name
to[].variablesobjectNoPer-recipient template variables
subjectstringConditionalRequired unless using a template
templateIdstringNoUse a stored template instead of inline content
htmlContentstringConditionalHTML body (htmlContent or textContent required if no templateId)
textContentstringNoPlain text fallback
variablesobjectNoGlobal template variables. Applied whether or not templateId is set; on a key overlap, the recipient's own to[].variables wins
replyTostringNoReply-to email address
tagsarrayNoTags for filtering in analytics and webhooks
customHeadersobjectNoCustom email headers (key-value pairs)
trackingobjectNoOverride tracking settings
tracking.opensbooleanNoAsk for open tracking on this message (default: off on both streams). Used only while the sending domain has made no choice; a domain's own per-stream choice, set on the Domains page, wins. false always turns it off
tracking.clicksbooleanNoEnable click tracking (default: false for transactional streams unless your account enables it, true for marketing)
ipPoolIdstringNoReserved for dedicated IP pools. Not yet available: sending it returns 400 IP_POOL_NOT_FOUND. Omit it.
scheduledAtdatetimeNoSchedule for future delivery (UTC), up to 7 days ahead
idempotencyKeystringNoPrevent duplicate sends for the same key. Prefer the X-Idempotency-Key header
attachmentsarrayNoFile attachments (max 10, max 10 MiB decoded total, see Attachments)
priorityenumNoLow, Normal (default), High, or Critical

Attachments

Each attachment requires three fields:

Attachment format
{
  "attachments": [
    {
      "fileName": "invoice.pdf",
      "contentType": "application/pdf",
      "content": "JVBERi0xLjQKJ...",
      "inline": false
    },
    {
      "fileName": "logo.png",
      "contentType": "image/png",
      "content": "iVBORw0KGgoAAAA...",
      "contentId": "company-logo",
      "inline": true
    }
  ]
}
FieldTypeRequiredDescription
fileNamestringYesFile name with extension
contentTypestringYesMIME type (e.g., application/pdf)
contentstringYesStandard Base64-encoded file content: padded, with no whitespace or line breaks
contentIdstringNoContent ID for inline attachments (reference in HTML as cid:company-logo)
inlinebooleanNoWhether this is an inline image (default: false)

At most 10 attachments per email, and at most 10 MiB decoded (post-Base64) in total. A missing contentType, non-canonical Base64, exceeding the count, or exceeding the size returns 400 VALIDATION_FAILED. POST /v1/transactional/send-batch has no attachments field.

Response

Status: 202 Accepted

Success response
{
  "success": true,
  "jobId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "messageIds": ["t_a1b2c3d4e5f6..."],
  "acceptedRecipients": ["[email protected]"],
  "rejectedRecipients": [],
  "queuedAt": "2026-02-17T10:30:00Z",
  "isSandboxed": false
}
FieldTypeDescription
successbooleanWhether the request was accepted
jobIdstringUnique job identifier for tracking
messageIdsarrayPer-recipient message IDs (t_ prefix for transactional, m_ for marketing)
acceptedRecipientsarrayEmail addresses accepted for delivery
rejectedRecipientsarrayRecipients that were rejected
queuedAtdatetimeWhen the email was queued
isSandboxedbooleantrue if the request used a sandbox credential (see Sandbox Mode)
sandboxReasonstringcredential_sandbox_mode (logged, not delivered) or all_recipients_not_in_sandbox_allowlist (no recipient was on the allow list); absent otherwise

Rejected Recipients

Recipients can be rejected for several reasons:

Response with rejected recipients
{
  "success": true,
  "jobId": "...",
  "acceptedRecipients": ["[email protected]"],
  "rejectedRecipients": [
    {
      "email": "[email protected]",
      "reason": "Suppressed"
    },
    {
      "email": "[email protected]",
      "reason": "domain_typo",
      "suggestion": "[email protected]"
    }
  ]
}

Common rejection reasons:

  • Suppressed: address is on the suppression list (previous bounces/complaints)
  • disposable_domain: disposable email provider detected
  • domain_typo: likely typo in domain (includes suggestion field)

Pre-flight Check

Verify your ability to send before making the actual request:

GET https://api.mail.land/v1/transactional/send/check?recipientCount=1
Pre-flight check
curl https://api.mail.land/v1/transactional/send/check?recipientCount=1 \
  -H "X-API-Key: ml_pet_Ab3Kx9mN2pQ7rT4wL8yD..."
Pre-flight response
{
  "canSend": true,
  "rateLimitStatus": {
    "currentSecond": 2,
    "limitPerSecond": 50,
    "currentHour": 450,
    "limitPerHour": 5000,
    "currentDay": 8200,
    "limitPerDay": 50000
  },
  "monthlyUsed": 125000,
  "monthlyLimit": 500000
}

If canSend is false, the reason field explains why (rate limit, quota, account status, etc).

Idempotency

Retrying a send after a network timeout must not deliver the email twice. Supply an idempotency key and a retry replays the original response instead of sending again.

Send the key as a header (recommended):

X-Idempotency-Key: order-12345-shipped-v1

Or use the idempotencyKey body field. Both are accepted, and you may send both only if the values are identical.

Rules

  • Scope. X-Idempotency-Key is accepted on POST /v1/transactional/send and POST /v1/transactional/send-batch only. No other endpoint reads it.
  • Format. 1–255 characters, letters, digits, _ and -. Keys are compared exactly: no trimming, no case folding.
  • Window. A key stays replayable for 24 hours. After that the same key is treated as a fresh request.
  • Replay. A retry within the window returns the original response verbatim (same IDs, same status) and sends nothing.
  • Uniqueness. Keys are unique per account. Reusing one key for a different endpoint is rejected with 409; use a distinct key per operation.

Correlation ID

Track requests across systems by including a correlation ID:

X-Correlation-ID: my-app-order-12345

or

X-Request-ID: my-app-order-12345

The correlation ID appears in logs and the activity stream.

Rate Limit Headers

Successful responses include per-IP rate limit information:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 295

When rate limited (429) by the send policy engine, a Retry-After header indicates seconds to wait:

Retry-After: 30

A transactional request may name up to 5 recipients, and each one counts against your rate limits. If that is more than your per-second allowance (for example 1 per second on a plan of 60 per minute), the request is accepted when no other request was accepted in the same second; otherwise it gets 429 with Retry-After: 1 and succeeds when retried in the next second, within your per-minute limit. See Rate Limits.

An IP-level 429 also includes X-RateLimit-Reset as an ISO-8601 UTC time.

Error Responses

All errors return a JSON body:

Error response format
{
  "code": "ERROR_CODE",
  "message": "Human-readable description",
  "timestamp": "2026-02-17T10:30:00Z",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Error Codes

StatusCodeDescription
400INVALID_REQUESTMalformed request body, or scheduledAt more than 7 days ahead ("Cannot schedule email more than 7 days in advance")
400VALIDATION_FAILEDNo htmlContent, textContent, or templateId; or an attachment fails validation (see Attachments). Response includes an errors[] array with the specific field and rule that failed. (Previously: MISSING_CONTENT: deprecated 2026-04.)
413CONTENT_TOO_LARGEThe assembled message would exceed the platform's message-size limit ("The message is too large to send: it would exceed the platform's message-size limit. Reduce the HTML, text, attachments or recipients."). Nothing is queued
400transactional_recipient_limit_exceededMore than 5 recipients on a transactional credential (1,000 on marketing)
400TEMPLATE_NOT_FOUNDtemplateId names no active template of this account (also returned for a deleted template)
400IP_POOL_NOT_FOUNDipPoolId names no pool assigned to this account (currently always, since no pool can be assigned yet, see the field reference above)
422TEMPLATE_VARIABLE_MISSINGThe content has a {{placeholder}} with no value; details.missingVariables lists the names. Nothing is queued
400IDEMPOTENCY_KEY_INVALIDThe X-Idempotency-Key header is empty, malformed, over 255 characters, or sent more than once (a malformed idempotencyKey body field returns VALIDATION_FAILED instead)
400IDEMPOTENCY_KEY_CONFLICTHeader and idempotencyKey body field were both supplied and differ
400SEND_FAILEDAll recipients are suppressed (message: "All recipients are suppressed")
409IDEMPOTENCY_KEY_REUSEDKey is already in use for a different endpoint on this account
401UNAUTHORIZEDMissing, invalid, expired or revoked X-API-Key
403ACCOUNT_SUSPENDEDAccount has been suspended
403ACCOUNT_DISABLEDAccount has been disabled
403ACCOUNT_NOT_ACTIVEAccount is not active
403NO_ACTIVE_SUBSCRIPTIONAccount has no active subscription
403SUBSCRIPTION_EXPIREDAccount's subscription has expired
403IP_NOT_ALLOWEDRequest IP not in credential's allowed list
403SENDING_BLOCKEDSending is blocked for this account
403ACTION_REQUIREDAn account action is required before sending
403TENANT_APPROVAL_REQUIREDThe account is awaiting review before it can send. Retrying does not help; contact support
422DOMAIN_NOT_VERIFIEDSender domain not verified
422DOMAIN_NOT_ALLOWED_FOR_CREDENTIALDomain not in credential's scope
422DOMAIN_NOT_FOUNDSender domain not found
429RATE_LIMIT_EXCEEDEDRate limit exceeded
429QUOTA_EXCEEDEDMonthly quota exhausted
429WARMUP_LIMIT_EXCEEDEDThe account's daily ramp-up limit for new sending was reached; resets at 00:00 UTC
429DAILY_LIMIT_EXCEEDEDDaily send limit reached
429WARMUP_POOL_BUDGET_EXCEEDEDPlatform capacity. Message: "Sending is temporarily paused for today. It resumes automatically at 00:00 UTC; no action is needed." (or "Sending is briefly delayed. Please retry in a few minutes." with a short Retry-After)
429WARMUP_POOL_EMPTYPlatform capacity. Message: "Sending is temporarily paused. It resumes automatically; no action is needed."
429PLATFORM_DAILY_CAPACITY_REACHEDPlatform sending capacity for today is fully booked; resets at 00:00 UTC. Not your account quota
429PLATFORM_CAPACITY_PROTECTIONNew sending is briefly paused to protect platform capacity; retry shortly
429PROBATION_CAP_REACHEDA new account's rolling 24-hour recipient cap was reached; capacity returns as earlier sends age out
429PAYMENT_DAILY_LIMIT_EXCEEDEDDaily limit reached on a past-due account
429MARKETING_PAUSEDMarketing sending paused for this account
429SENDING_BLOCKEDAccount verification level doesn't allow this send (sandbox/verification refusals; a hard sending block instead returns 403, see above)
503SERVICE_UNAVAILABLEEmail service temporarily unavailable

The 429 platform-capacity rows are not caused by your own usage. Nothing was queued, Retry-After says when to retry, and no action is needed on your side; see Error Codes for the exact messages. Marketing-only refusals (MARKETING_NOT_YET_ALLOWED, MARKETING_SUSPENDED, both 403) are listed there too.

Transactional Recipient Limit

Transactional credentials enforce a maximum of 5 recipients per send request. This prevents misuse of transactional streams for bulk sending. If you need more recipients, use a Marketing credential with batch sending.

Using Templates

Instead of inline HTML, reference a stored template:

Send with a template
{
  "from": { "email": "[email protected]" },
  "to": [
    {
      "email": "[email protected]",
      "variables": {
        "firstName": "Jane",
        "resetLink": "https://app.example.com/reset?token=abc123"
      }
    }
  ],
  "templateId": "t8x2m4q9zk7wpr",
  "variables": {
    "companyName": "Acme Inc"
  }
}

The subject comes from the template. On a key overlap, the recipient's own to[].variables overrides the global variables: the same rule as inline content. See Templates for creating and managing templates.

Next Steps

On this page