Send a message
/api/v1/messagesSubmits 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:
- The domain in
fromis verified. See Managing domains and DNS. fromis 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.- 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
422and records anemail.droppedevent.
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
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Makes 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
toanyrequiredRecipients: one address or a list, each
user@example.comorName <user@example.com>. At most 50 acrossto,ccandbcctogether. Use/messages/batchfor more, or when each recipient needs their own values.fromstringrequiredMust 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_tostringWhere replies go instead of
from. Accepts a display name.headersobjectCustom headers, at most 25, such as
In-Reply-ToandReferencesto 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.
namestringrequiredFilename as the recipient sees it. Its extension is checked against the blocked list.
contentstringrequiredThe file, base64 encoded.
content_typestringMIME type. Guessed from the filename when omitted.
content_idstringSet this to show the file inline instead of attaching it, and use it in your HTML as
<img src="cid:THE_VALUE">.
subjectstringRequired unless template_id is given.
htmlstringHTML body. Required unless template_id or text is given.
textstringPlain-text body. Always worth sending alongside html.
template_idstringA stored template to render instead of inline content.
variablesobjectValues 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.metadataobjectYour 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.
trackingbooleanOpen 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.streamstringmarketingSend over the marketing egress IP instead of the transactional one. Omit for application mail.
Request
/api/v1/messagescurl -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"
}
]
}'const response = await fetch('https://app.mailyte.com/api/v1/messages', {
method: 'POST',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"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"
}
]
}),
});
const { data } = await response.json();import requests
response = requests.post(
"https://app.mailyte.com/api/v1/messages",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
json={
"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"
}
]
},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->post('https://app.mailyte.com/api/v1/messages', [
'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}}
Your 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',
],
],
]);
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/messages")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
"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"
}
]
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
The message was accepted for delivery.
dataobjectobjectstringmessageidstringMailyte's ID for this message: a ULID such as
01JBT8XQ2M9WYC3K4F6R7S8T9V, without angle brackets. Look it up right away withGET /email-logs/messages/{id}. It is not the Message-ID header.message_idstringThe Message-ID header as sent, angle brackets included, such as
<01JBT8XQ2M…@example.com>. Use it, percent-encoded, withGET /domains/{domain}/messages/{messageId}. It is not the same asid.recipientstringThe first recipient's address, without a display name. Read
recipientsfor everyone the message was sent to.recipientsobjectEveryone 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.
countintegerTo plus Cc. Excludes Bcc, which is not published here; the limit of 50 counts all three together.
reply_tostringThe Reply-To address sent. Null when none was set; replies then go to
from.attachment_countintegerHow many files were attached.
fromobjectThe sender. An object here, and everywhere else a sender appears, so one piece of caller code reads all of them.
emailstringThe address the message was sent from.
namestringThe display name in the
From:header. Null when the sender has no display name, so the message went out as a bare address.
subjectstringThe subject as sent, with
{{ }}variables filled in. An empty subject is"", never null.submitted_atstringWhen we accepted the message. This is not delivery: a 200 means queued, and the outcome arrives as a delivery event or a webhook.
modestringlive | testlivefor a real send.testwhen 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 withGET /sandbox/messages/{id}. Test sends skip suppression and rate limits, and fire no live webhooks.sandboxobjectWhere a test send was stored. Null for a live send.
message_idstringThe sandbox message id, for
GET /sandbox/messages/{id}. Not the same asid: that is the Message-ID local part the message carries, this is the sandbox's own record of it.inbox_idstringThe inbox it landed in: your default sandbox inbox, created for you on the first test send if you had none.
application/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
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The 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. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too 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.