About this page · Updated 2026-10-22 16:14 UTC
What this page is: Transactional email API over HTTPS — Motorical POST /v1/send with Motor Block API keys, dry-run validation, and delivery checks.
How far to trust this page: the build checked 13 names, paths, tools, scopes and error codes against the real product (0 do not match). Reviewed claim by claim on 2026-10-22 (documentation truth audit).
Updated 2026-10-22 16:14 UTCLatest change: Send Email pages: every field, shown the same wayRecently updated pages
Transactional Email API (HTTP Send)
Use Motorical's transactional email API (POST /v1/send) when your application wants to send email over HTTPS instead of SMTP. Authenticate with a Motor Block API key. Start with dryRun: true to validate the same payload without queueing an email. dryRun defaults to false on this endpoint, so the request is sent unless you pass dryRun: true; always set it explicitly.
What You Need
from address must use the domain attached to the Motorical SMTP Motor Block.mk_live_... key, not an Account API Key.info@yourdomain.com. Optional fromName sets the inbox display name (do not put a display name in from or a From header).:::tip Key distinction
POST /v1/send uses a Motorical SMTP Motor Block API Key: Authorization: ApiKey mk_live_....
Do not use a Public API bearer token or an Account API Key for sending. :::
Request fields
Every field of POST /v1/send, generated from the API definition. The examples below show the common ones; the table is the full list.
| Field | Type | Required | Meaning |
|---|---|---|---|
from | string | Yes | Sender address (must match motor block domain). Bare email only — use fromName for display name. |
fromName | string | No | Optional From display name (e.g. "Acme Billing"). Produces From: "Acme Billing" addr@domain. Do not set a From custom header (DISALLOWED_HEADERS). |
to | string or array of string | Yes | Recipient address, or a list of addresses. One request is one message: every recipient receives the same message and the To header shows the addresses as written. For a separate message per person, send one request each. |
cc | string or array of string | No | Copy recipients, named in the Cc header. to, cc and bcc together take at most 50 addresses. Rolling out per Motor Block: 422 feature_not_enabled where not available. |
bcc | string or array of string | No | Blind-copy recipients: they receive the message but are named in no header. Rolling out per Motor Block. |
replyTo | string | No | Reply-To address. Not allowed together with a Reply-To custom header (422 conflicting_fields). Rolling out per Motor Block. |
subject | string | Yes | Email subject |
text | string | No | Plain text body |
html | string | No | HTML body |
headers | object | No | Custom headers applied to the outgoing message and DKIM-signed (e.g. In-Reply-To, References, Reply-To, List-Unsubscribe, custom X-). Reserved/spoofable headers are rejected with 400 DISALLOWED_HEADERS: From, Sender, Return-Path, To, Cc, Bcc, Subject, Date, Message-ID, MIME-Version, Content-, DKIM-Signature, Received, Authentication-Results, ARC-, X-Motorical-. Header values containing CR/LF are also rejected. |
attachments | array of object | No | Files to attach. Your plan decides how many, how large (per file and in total) and which types, exactly as over SMTP; a refusal names the plan and the limit. Rolling out per Motor Block: 422 feature_not_enabled where not available. |
dryRun | boolean | No | Validate without queueing or writing email logs. With attachments it also runs the plan check (not the virus and content scan). |
Idempotency-Key (header) | string | No | Unique key to prevent duplicate sends (24h window) |
1. Dry-Run the Payload
Dry run validates authentication, sender domain, DNS readiness, recipient shape, subject, and body size. It does not create an email log or queue delivery work.
- curl
- Node.js
- Python
- PHP
- Go
curl -X POST "https://api.motorical.com/v1/send" \
-H "Authorization: ApiKey $MK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Dry run check",
"text": "Validate this email without sending it.",
"html": "<p>Validate this email without sending it.</p>",
"dryRun": true
}'
const response = await fetch('https://api.motorical.com/v1/send', {
method: 'POST',
headers: {
Authorization: `ApiKey ${process.env.MK_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'sender@yourdomain.com',
fromName: 'Acme Billing',
to: ['recipient@example.com'],
subject: 'Dry run check',
text: 'Validate this email without sending it.',
html: '<p>Validate this email without sending it.</p>',
dryRun: true
})
});
console.log(await response.json());
import os
import requests
response = requests.post(
"https://api.motorical.com/v1/send",
headers={
"Authorization": f"ApiKey {os.environ['MK_API_KEY']}",
"Content-Type": "application/json",
},
json={
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Dry run check",
"text": "Validate this email without sending it.",
"html": "<p>Validate this email without sending it.</p>",
"dryRun": True,
},
timeout=30,
)
print(response.json())
<?php
$payload = [
"from" => "sender@yourdomain.com",
"fromName" => "Acme Billing",
"to" => ["recipient@example.com"],
"subject" => "Dry run check",
"text" => "Validate this email without sending it.",
"html" => "<p>Validate this email without sending it.</p>",
"dryRun" => true,
];
$ch = curl_init("https://api.motorical.com/v1/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: ApiKey " . getenv("MK_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
payload := map[string]any{
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": []string{"recipient@example.com"},
"subject": "Dry run check",
"text": "Validate this email without sending it.",
"html": "<p>Validate this email without sending it.</p>",
"dryRun": true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.motorical.com/v1/send", bytes.NewReader(body))
req.Header.Set("Authorization", "ApiKey "+os.Getenv("MK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status)
}
Expected dry-run response:
{
"success": true,
"dryRun": true,
"data": {
"status": "validated",
"acceptanceStatus": "validated",
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"domain": "yourdomain.com",
"recipientCount": 1,
"bodySize": 42
}
}
2. Send the Email
Remove dryRun or set it to false. Add an Idempotency-Key header if your application may retry the request.
curl -X POST "https://api.motorical.com/v1/send" \
-H "Authorization: ApiKey $MK_API_KEY" \
-H "Idempotency-Key: order-12345-welcome-email" \
-H "Content-Type: application/json" \
-d '{
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Welcome to our app",
"text": "Thanks for signing up.",
"html": "<p>Thanks for signing up.</p>"
}'
A successful send returns data.id (the send UUID) and data.acceptanceStatus: "queued". Keep data.id — it is what the message lookup and timeline endpoints take.
3. Check Delivery
Use the Public API to inspect logs and timeline events. These endpoints use a short-lived bearer token minted with an Account API Key.
Use the data.id returned by POST /v1/send as $SEND_ID.
curl -H "Authorization: Bearer $TOKEN" \
"https://api.motorical.com/api/public/v1/messages/$SEND_ID/events"
For real-time automation, create a webhook for message.delivered, message.bounced, and message.complained.
4. Copies, a reply address, headers and attachments
POST /v1/send also takes cc, bcc, replyTo and attachments. They are rolling out per Motor Block: A Motor Block that has not been moved to the new sending path answers 422 with code feature_not_enabled and sends nothing (the fields are never silently dropped), so use SMTP there. See Send attachments.
curl -X POST "https://api.motorical.com/v1/send" \
-H "Authorization: ApiKey $MK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "billing@yourdomain.com",
"fromName": "Acme Billing",
"to": "customer@example.com",
"cc": ["accounts@example.com"],
"bcc": "archive@yourdomain.com",
"replyTo": "support@yourdomain.com",
"subject": "Your invoice",
"text": "Your invoice is attached."
}'
cc recipients are named in the Cc header; bcc recipients receive the message but are named in no header. A recipient on the suppression list is skipped and listed in data.skipped.
Your own headers go in headers (for example to thread a reply, or to add one-click unsubscribe). They are DKIM-signed; reserved headers such as From and To are refused with 400 DISALLOWED_HEADERS (the full list is under Custom headers):
curl -X POST "https://api.motorical.com/v1/send" \
-H "Authorization: ApiKey $MK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "support@yourdomain.com",
"fromName": "Acme Support",
"to": "customer@example.com",
"subject": "Re: Your question",
"text": "Thanks for reaching out.",
"headers": {
"In-Reply-To": "<CABc123@mail.gmail.com>",
"References": "<CABc123@mail.gmail.com>",
"List-Unsubscribe": "<mailto:unsubscribe@yourdomain.com>"
}
}'
A file goes in attachments as base64. Build the body with a tool that does the encoding for you, so a large file never has to fit on a command line:
jq -n --arg content "$(base64 < invoice.pdf | tr -d '\n')" '{
from: "billing@yourdomain.com", fromName: "Acme Billing", to: "customer@example.com",
subject: "Your invoice", text: "Your invoice is attached.",
attachments: [{ filename: "invoice.pdf", content: $content, contentType: "application/pdf" }]
}' | curl -X POST "https://api.motorical.com/v1/send" \
-H "Authorization: ApiKey $MK_API_KEY" -H "Content-Type: application/json" --data-binary @-
Run it with "dryRun": true first: it runs the same plan check as a real send (number of files, size per file, total, file types) and answers with the refusal a real send would get. It does not run the virus and content scan, so a real send can still be refused for the file itself. The full field list, the limits and every error are in Send attachments.
:::note What this endpoint does not do
It does not take a file by reference: there is no upload step yet, so the whole file travels base64-encoded in the request body. You can set In-Reply-To, References and List-Unsubscribe through the headers field.
:::
Common Errors
| Error | What it means | Fix |
|---|---|---|
401 Invalid API key format | The key is missing, truncated, or the wrong type. | Use a full mk_live_... Motorical SMTP Motor Block API Key. |
403 From address domain does not match | Sender domain is not attached to the Motorical SMTP Motor Block. | Send from the verified SMTP Motor Block domain. |
403 Domain DNS setup incomplete | SPF or DKIM is not ready. | Complete domain DNS setup before sending. |
422 Validation error | Required fields or body content are missing. | Include from, to, subject, and at least one of text or html. |
429 rate_limited_block | This Motor Block's burst/hourly/daily send cap was exceeded. | Back off and retry after retryAfterSeconds / the Retry-After header. Check GET /motor-blocks/{id}/rate-limits. |
429 rate_limited_account | Your account-wide send cap was exceeded (enforced). | Check GET /account/rate-limits and back off. |
See Rate Limits & Retention for the full 429 contract, including headers and how each limit is enforced.