Send a message

POST/api/v1/messages
Requires anorganization API keywith the scopemessages:send

Submits one message for delivery and returns as soon as it is accepted — delivery itself happens asynchronously, so a 200 here means "queued", not "in their inbox". Watch for the outcome through delivery events or a webhook.

Three things have to be true before a send will work, and they are the cause of almost every first-attempt failure:

  1. The domain in from is verified. See Managing domains and DNS.
  2. from is a verified sender belonging to your organization. An address that merely sits on a verified domain is not enough; it has to exist as a sender.
  3. The recipient is not suppressed. Mailyte refuses sends to addresses that have bounced, complained or unsubscribed, and it refuses them before your own list logic gets a say. A suppressed recipient returns 422 and records an email.dropped event.

Provide content either directly (subject plus html and/or text) or by naming a template_id. If you do both, the fields you pass win over the template's. variables are substituted in either case.

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringMakes this request safe to retry. Use <app>-<ULID>, such as crm-01JBT8XQ2M9WYC3K4F6R7S8T9V; any other shape is a 422. Send the same key on every retry: a repeat gets the first answer with Idempotency-Replayed: true, a key reused for a different request is a 422, and one still running is a 409 with Retry-After. Keys last 24 hours, per organization.

Request body

  • toanyrequired

    Recipients: one address or a list, each user@example.com or Name <user@example.com>. At most 50 across to, cc and bcc together. Use /messages/batch for more, or when each recipient needs their own values.

  • fromstringrequired

    Must be a verified sender in your organization. The display name comes from the sender record.

  • ccarray<string>

    Carbon-copy recipients. Counts toward the same 50.

  • bccarray<string>

    Blind-copy recipients, counted in the same 50. No recipient can see them, and they are not listed in the response.

  • reply_tostring

    Where replies go instead of from. Accepts a display name.

  • headersobject

    Custom headers, at most 25, such as In-Reply-To and References to thread a conversation. Headers Mailyte sets itself (From, To, Subject, Message-ID, DKIM-Signature, List-Unsubscribe, Bcc, X-Mailyte-*) are refused with a 422.

  • attachmentsarray<object>

    Files to attach. The whole message may be at most 10 MB, measured AFTER base64 encoding — the encoding inflates by roughly a third, so budget accordingly. Windows executable and script extensions are refused.

    • namestringrequired

      Filename as the recipient sees it. Its extension is checked against the blocked list.

    • contentstringrequired

      The file, base64 encoded.

    • content_typestring

      MIME type. Guessed from the filename when omitted.

    • content_idstring

      Set this to show the file inline instead of attaching it, and use it in your HTML as <img src="cid:THE_VALUE">.

  • subjectstring

    Required unless template_id is given.

  • htmlstring

    HTML body. Required unless template_id or text is given.

  • textstring

    Plain-text body. Always worth sending alongside html.

  • template_idstring

    A stored template to render instead of inline content.

  • variablesobject

    Values for {{ name }} placeholders in the subject, HTML and text, such as {{ first_name | default('there') }}. Values are HTML-escaped in the HTML body. Mailchimp merge tags such as *|FNAME|* work too.

  • metadataobject

    Your own data, returned on every delivery event for this message and never shown in it, such as your order ID. At most 10 fields; keys up to 20 characters, values up to 80.

  • tagsarray<string>

    Your own labels, returned on the delivery events for this message. At most 10.

  • trackingboolean

    Open and click tracking for this message. Left out, it is off for application mail and on for stream: marketing. Untracked links stay exactly as you wrote them.

  • streamstringmarketing

    Send over the marketing egress IP instead of the transactional one. Omit for application mail.

Request

POST/api/v1/messages
curl -X POST 'https://app.mailyte.com/api/v1/messages' \
  -H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": [
      "Ada Lovelace <ada@example.com>"
    ],
    "cc": [
      "accounts@acme.com"
    ],
    "from": "hello@yourdomain.com",
    "reply_to": "support@yourdomain.com",
    "subject": "Your receipt from Acme",
    "html": "<h1>Thanks, {{first_name}}</h1><p>Your order is on its way.</p>",
    "text": "Thanks, {{first_name}}\nYour order is on its way.",
    "variables": {
      "first_name": "Ada"
    },
    "tags": [
      "receipt"
    ],
    "headers": {
      "X-Order-Id": "ord_1234"
    },
    "metadata": {
      "order_id": "ord_1234",
      "tier": "pro"
    },
    "attachments": [
      {
        "name": "invoice.pdf",
        "content": "JVBERi0xLjQKJ...",
        "content_type": "application/pdf"
      }
    ]
  }'
The key names its own organization, so no X-Organization-ID header is needed.

Response

The message was accepted for delivery.

  • dataobject
    • objectstringmessage
    • idstring

      Mailyte's ID for this message: a ULID such as 01JBT8XQ2M9WYC3K4F6R7S8T9V, without angle brackets. Look it up right away with GET /email-logs/messages/{id}. It is not the Message-ID header.

    • message_idstring

      The Message-ID header as sent, angle brackets included, such as <01JBT8XQ2M…@example.com>. Use it, percent-encoded, with GET /domains/{domain}/messages/{messageId}. It is not the same as id.

    • recipientstring

      The first recipient's address, without a display name. Read recipients for everyone the message was sent to.

    • recipientsobject

      Everyone the message was addressed to. Bcc recipients are never listed here; they appear only in your delivery log.

      • toarray<string>

        As accepted, including any display name.

      • ccarray<string>

        Empty when there were no cc recipients.

      • countinteger

        To plus Cc. Excludes Bcc, which is not published here; the limit of 50 counts all three together.

    • reply_tostring

      The Reply-To address sent. Null when none was set; replies then go to from.

    • attachment_countinteger

      How many files were attached.

    • fromobject

      The sender. An object here, and everywhere else a sender appears, so one piece of caller code reads all of them.

      • emailstring

        The address the message was sent from.

      • namestring

        The display name in the From: header. Null when the sender has no display name, so the message went out as a bare address.

    • subjectstring

      The subject as sent, with {{ }} variables filled in. An empty subject is "", never null.

    • submitted_atstring

      When we accepted the message. This is not delivery: a 200 means queued, and the outcome arrives as a delivery event or a webhook.

    • modestringlive | test

      live for a real send. test when the request used a test API key (mk_test_…): the message was checked and built like a real one, then stored in your Sandbox instead of being delivered. Read it with GET /sandbox/messages/{id}. Test sends skip suppression and rate limits, and fire no live webhooks.

    • sandboxobject

      Where a test send was stored. Null for a live send.

      • message_idstring

        The sandbox message id, for GET /sandbox/messages/{id}. Not the same as id: that is the Message-ID local part the message carries, this is the sandbox's own record of it.

      • inbox_idstring

        The inbox it landed in: your default sandbox inbox, created for you on the first test send if you had none.

200application/json
{
  "message": "Message submitted successfully",
  "code": 200,
  "success": true,
  "data": {
    "message_id": "<01JBT8XQ2M@yourdomain.com>",
    "event_id": "01JBT8XQ2M9WYC3K4F6R7S8T9V",
    "to": "ada@example.com",
    "from": "hello@yourdomain.com",
    "subject": "Your receipt from Acme",
    "submitted_at": "2026-09-17T10:04:11.000000Z"
  }
}

Returned inside the standard envelope.

Errors

StatusWhen
401The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist.
403The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys.
404No such resource in this organization.
422The request was understood but the values were not acceptable.
429Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait.

Every status, with what causes it and what to do, is on the error reference.