Skip to main content

API Endpoints

Public Analytics / email delivery API endpoints. All paths below are relative to the base URL:

https://api.motorical.com/api/public/v1

For interactive testing, use the Swagger UI.

Motor Blocks​

GET /motor-blocks​

Lists all motor blocks you have access to. Use this first to discover your block IDs.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks"

Response: Array of motor blocks with names, IDs, and basic info.


GET /motor-blocks/{id}/overview​

One-call overview with totals, rates, reputation, rate limits, anomalies, and top domains.

Scopes: analytics.read, usage.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/overview"

Ideal for dashboards, health checks, and monitoring UIs that need a compact summary.


GET /motor-blocks/{id}/daily-summary​

Daily rollups over a recent window (sent, delivered, bounced, complained + rates).

Parameters:

ParameterDescription
daysNumber of days to include (up to your retention window)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/daily-summary?days=30"

GET /motor-blocks/{id}/rate-limits​

Current usage against this Motor Block's effective ceiling — the lower of the block's configured limits and any active reputation-based throttle. This is not your plan ceiling; the plan drives the account-wide cap below.

Scopes: usage.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/rate-limits"

Response: burst, hourly, and daily objects (each used, limit, remaining, percentage, resetIn), plus:

FieldMeaning
limitsYour raw configured limits, unaffected by throttling — compare with daily.limit to see how far a throttle has lowered the ceiling
throttledtrue when a system throttle is currently lowering the effective ceiling
throttleReasonWhy, or null
timestampWhen the counters were read

A top-level warning field (outside data) appears only when counter data was temporarily unavailable; the bucket values are then ceiling-only estimates, not live counts. Note that GET /account/rate-limits carries its warning inside data.


GET /account/rate-limits​

Current usage vs your account-wide hourly and daily send caps, aggregated across every Motor Block on your account. Unlike the endpoint above, this is not scoped to a single Motor Block — the token's bound motorBlockId only identifies which account it belongs to.

Scopes: usage.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/rate-limits"

Response:

{
"success": true,
"data": {
"source": "plan",
"timestamp": "2026-08-24T14:31:38.509Z",
"hourly": { "used": 46, "limit": 1500, "remaining": 1454, "resetIn": 1702 },
"daily": { "used": 611, "limit": 10000, "remaining": 9389, "resetIn": 34162 }
}
}

source is "plan" when the ceiling comes from your subscription, or "fallback" for accounts without an active product. A warning field appears (with hourly/daily values as best-effort fallback estimates, not live counts) only if usage data was temporarily unavailable. See Rate Limits & Retention for how this account-wide ceiling relates to the per-block one above and to POST /v1/send's enforcement.


GET /account/state​

GET /api/public/v1/account/state — one composed answer for "where does this account stand": domain verification, DNS readiness, Motor Block existence, and sandbox status, in a single call instead of stitching together /domains, /motor-blocks, and /account/sandbox yourself. Account-wide — no motorBlockId needed, and works before any Motor Block exists.

Scopes: config.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/state"

GET /motor-blocks/{id}/metrics​

Email performance data (sent, delivered, bounced, complained) with time-series breakdowns.

Parameters:

ParameterDescription
intervalhour or day (required)
fromStart time (ISO 8601)
toEnd time (ISO 8601)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/metrics?interval=hour&from=2024-01-15T00:00:00Z&to=2024-01-16T00:00:00Z"

GET /motor-blocks/{id}/logs​

Search individual email events and records.

Parameters:

ParameterDescription
querySearch filter (see query syntax below)
statussent, delivered, bounced, complained, deferred, failed
fromStart time (ISO 8601)
toEnd time (ISO 8601)
limitMax results (1–1000, default 100)
cursorFor pagination (use nextCursor from response)

Query Syntax:

  • recipient:user@example.com — specific email address
  • domain:gmail.com — all emails to Gmail
  • status:bounced — only bounced emails
  • metadata.campaign_id:newsletter-2024 — by campaign metadata
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/logs?query=domain:gmail.com&status=bounced&limit=50"

GET /motor-blocks/{id}/providers​

Aggregate performance by mailbox provider (gmail, outlook, yahoo, other).

Parameters:

ParameterDescription
fromStart time (ISO 8601)
toEnd time (ISO 8601)
limitMax providers to return (default 20)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/providers?from=2024-01-01T00:00:00Z&to=2024-01-08T00:00:00Z"

GET /motor-blocks/{id}/error-codes​

Summarize SMTP response codes with human-friendly categories and hints.

Parameters: from, to, limit (default 50)

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/error-codes?from=2024-01-01T00:00:00Z&to=2024-01-08T00:00:00Z"

GET /motor-blocks/{id}/config​

Read-only snapshot of a motor block's non-sensitive configuration and domain status.

Scopes: config.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/config"

GET /motor-blocks/{id}/domain-health​

Summarized SPF/DKIM/DMARC and verification status for the attached domain.

Cache-first: the default response uses DB auth flags and returns quickly. Stale or never-checked domains schedule an async DNS refresh; the next call picks up updated flags. Pass refresh=true to wait for a live re-check (time-capped).

Scopes: config.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/domain-health"

GET /motor-blocks/{id}/deliverability​

Deliverability matrix broken down by recipient domain. Shows delivery, bounce, complaint, and failure rates per domain.

Scopes: analytics.read

Parameters:

ParameterDescription
fromStart time (ISO 8601)
toEnd time (ISO 8601)
limitMax domains to return (1–200, default 50)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/deliverability?from=2024-01-01T00:00:00Z&to=2024-01-08T00:00:00Z&limit=20"

Response: Array of domains with total, delivered, bounced, complained, failed, and deliveryRate per domain.


GET /motor-blocks/{id}/reputation​

Reputation score (0–100) calculated from bounce, complaint, and failure rates over your retention window.

Scopes: analytics.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/reputation"

Response: score (0–100), windowDays, totals (total, delivered, bounced, complained, failed), and rates (bounceRate, complaintRate, failRate as percentages).


GET /motor-blocks/{id}/anomalies​

Bounce spike detection over a 14-day window. Flags anomalies when today's bounce rate exceeds 2× the historical average by more than 2 percentage points.

Scopes: analytics.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/anomalies"

Response: anomalies array (empty if none detected; each entry has type, todayRate, avgRate), and windowDays.


Messages​

GET /messages/{id}​

GET /api/public/v1/messages/{id} — look up a single message by send id (UUID from POST /v1/send).

Scopes: logs.read

Parameters:

ParameterDescription
includePIItrue/false (requires logs.pii scope for unmasked recipients)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/messages/$MESSAGE_ID"

Response:

FieldMeaning
idSend UUID (the same id used to look this message up)
smtpMessageIdRFC Message-ID header value, or null
currentOutcomeCurrent delivery outcome (e.g. queued, delivered, bounced, failed)
isTerminaltrue once the message has reached a final outcome
fromSender address
recipientRecipient address (masked unless includePII=true with logs.pii scope)
subjectMessage subject
queuedAtWhen the message was queued
processedAtWhen the message was processed, or null
deliveredAtWhen delivery was confirmed, or null
bouncedAtWhen a bounce was recorded, or null
smtpCodeSMTP response code from the receiving server, or null
smtpResponseFull SMTP response message, or null

GET /messages​

Lookup a message by RFC Message-ID header. Not a collection endpoint — smtpMessageId is required. To list recent sends use GET /motor-blocks/{id}/logs. To look up by send UUID use GET /messages/{id}.

Scopes: logs.read

ParameterRequiredDescription
smtpMessageIdYesRFC Message-ID (with or without angle brackets)
includePIINoRequires logs.pii
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/messages?smtpMessageId=$SMTP_MESSAGE_ID"

GET /messages/{id}/events​

GET /api/public/v1/messages/{id}/events — canonical lifecycle events for a single message (queued → final result).

Scopes: logs.read

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/messages/$MESSAGE_ID/events"

Response: Lifecycle events from email_events (e.g. queued → delivered/bounced).

Use the messageId from your own logs or from /logs responses.


GET /messages/{id}/recipients​

GET /api/public/v1/messages/{id}/recipients — per-recipient outcomes for a multi-recipient send. GET /messages/{id} and GET /messages/{id}/events both collapse to a single email_events row (whichever recipient's log line landed first); this is the only endpoint that reads postfix_log_events grouped by recipient, so it is the one place a multi-recipient send's individual outcomes are visible.

Scopes: logs.read

Parameters:

ParameterDescription
includePIItrue/false (requires logs.pii scope for unmasked recipients)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/messages/$MESSAGE_ID/recipients"

Response:

FieldMeaning
recipientsArray of per-recipient outcomes (address masked unless includePII=true with logs.pii)
expectedCountHow many recipients this send was addressed to
allTerminaltrue once every recipient has reached a final outcome
pii_maskedtrue when addresses in this response are masked
pii_unmask_pathHow to re-request this data with addresses unmasked, or null when already unmasked

Webhooks​

Full webhook CRUD is available. See the Webhooks reference for detailed examples including signature verification.

Scopes: All webhook endpoints require webhooks.manage.

GET /motor-blocks/{id}/webhooks​

List all webhook endpoints for a motor block.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/webhooks"

POST /motor-blocks/{id}/webhooks​

Create a new webhook endpoint.

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhook","events":["message.delivered"]}' \
"$BASE_URL/motor-blocks/$BLOCK_ID/webhooks"

PUT /motor-blocks/{id}/webhooks/{webhookId}​

Update an existing webhook (url, events, or enabled status).


DELETE /motor-blocks/{id}/webhooks/{webhookId}​

Delete a webhook endpoint.


POST /motor-blocks/{id}/webhooks/{webhookId}/test​

Queue a test delivery to verify your endpoint is working.


GET /motor-blocks/{id}/webhooks/{webhookId}/deliveries​

View recent delivery attempts for a webhook.

Parameters: limit (1–200, default 50)


GET /motor-blocks/{id}/webhooks/{webhookId}/stats​

High-level stats for webhook delivery (delivered, failed, queued, failure rate, latency).

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/webhooks/$WEBHOOK_ID/stats?hours=24"

Parameters: hours (1–168, default 24)


Exports​

See the Data Exports guide for a complete walkthrough.

GET /motor-blocks/{id}/exports/preview​

Lightweight preview of recent logs without creating a background export job.

Scopes: analytics.read, logs.read

Parameters:

ParameterDescription
limitMax records to return (default 100, max 500)
includePIItrue/false (requires logs.pii scope for unmasked recipients)
curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/exports/preview?limit=100"

POST /motor-blocks/{id}/exports​

Create a background export job. Returns 202 Accepted with the job ID.

Scopes: analytics.read

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jobType":"logs"}' \
"$BASE_URL/motor-blocks/$BLOCK_ID/exports"

GET /motor-blocks/{id}/exports​

List recent export jobs (up to 50) for this motor block.

Scopes: analytics.read


GET /exports/{jobId}​

Check the status of an export job. When complete, includes a download_url with a 15-minute download token.

Scopes: analytics.read


GET /exports/{jobId}/download​

Download a completed export as NDJSON. Requires the token query parameter from the job status response.

curl -o export.ndjson \
"$BASE_URL/exports/$JOB_ID/download?token=$DOWNLOAD_TOKEN"

Account Sandbox​

Account-scoped counterpart to the dashboard-only /api/developer/sandbox routes documented in the Local MCP Server guide — same underlying behavior, reachable with an OAuth Delegation header or Authorization: Bearer token instead of a dashboard session, so a connected agent can provision a sandbox without ever holding a dashboard JWT. Account-wide, like /account/rate-limits and /account/state above: a sandbox is provisioned for the account, not a specific Motor Block, and no block exists yet the first time /provision is called.

Scopes: sandbox.manage

GET /account/sandbox​

Current sandbox status for this account — whether one is provisioned, its handle, outbound-allowlist state, and whether the account already has an active production Motor Block. allowlist is the effective list (sandbox recipients, link-confirmed recipients, the account address and the test sink, minus any address that clicked stop). domain.sandbox true means the Motorical-owned sandbox domain; false means your own domain.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/sandbox"

POST /account/sandbox/provision​

Provision a Motorical-owned .sandbox domain handle (instant, no DNS) and a locked, outbound-allowlisted Motor Block. A Free account that adds and fully verifies its own domain gets a sandbox Motor Block on that domain automatically, with no provisioning call, when automatic provisioning is enabled (otherwise call this route); see Two ways to start. Idempotent — calling it again for an account that already has a sandbox returns the existing one (200) instead of creating a second (201 on first provision).

Parameters: handle (optional — a name is generated if omitted), channel (optional, defaults to agent)

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' \
"$BASE_URL/account/sandbox/provision"

POST /account/sandbox/convert​

Promote the sandbox Motor Block onto a verified domain, lifting the outbound allowlist lock. Requires the target domain to already be added and verified via POST /api/public/v1/domains and POST /api/public/v1/domains/{id}/verify (see the AI Agent Quickstart).

Parameters: domainId (required), name (optional production name; a deterministic one is chosen if omitted), type (optional, transactional or general_purpose)

Conversion is a one-way production identity transition. The block keeps its id, password, API keys, auth method and grant coverage, but takes the chosen production name and type, and its SMTP username is replaced. The response reports previousSmtpUsername, smtpUsername, credentialChanges (smtpUsernameChanged: true, everything else false) and integrationActionRequired — every SMTP client must switch to the new username. It never returns a credential.

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domainId":"$DOMAIN_ID","name":"Receipts","type":"transactional"}' \
"$BASE_URL/account/sandbox/convert"

Account Free Recipients​

Free accounts can send test email only to their own address and to recipients who have allowed it. These routes request that permission, list it, and remove it. They can never confirm for the recipient: the person must open the emailed link and click the button themselves. Account-wide, like the sandbox routes above.

Scopes: recipients.manage (MCP scope manage:recipients, separate from sandbox.manage).

GET /account/free-recipients​

Addresses this account has asked, with status, the recipient limit, and current usage (usage is null if it cannot be read). limitsNote explains that the hourly, daily and monthly numbers are the Free-plan caps for non-sandbox Motor Blocks; a sandbox Motor Block is governed by the sandbox caps shown in motorical_sandbox_status.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/free-recipients"

POST /account/free-recipients​

Email an address a confirmation link. Parameters: email (required). The success body names the address and its status in data.recipient ({ email, status }; pending until the recipient clicks the emailed link, confirmed if it already was). The answer is the same whether the email was sent or withheld, so it cannot be used to look up who has stopped. Errors: invalid_email and own_address (400), recipient_limit (409), rate_limited (429), email_send_failed (502).

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"friend@example.com"}' \
"$BASE_URL/account/free-recipients"

DELETE /account/free-recipients/{id}​

Remove a recipient. id must be a UUID from the list (400 invalid_id otherwise, 404 not_found if it is not yours).

curl -X DELETE -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/free-recipients/$RECIPIENT_ID"

Account Motor Blocks​

Production Motor Block lifecycle for OAuth-delegated agents and Public API bearer tokens — the routes behind the motorical_motor_block_* MCP tools. Only transactional and general_purpose blocks are managed; sandbox, marketing and portal blocks are refused. Which blocks you can touch is decided by the current authorization grant, read live on every call — never by a claim frozen in the token — so a block you create is usable straight away without refreshing or re-authorizing.

Scopes: config.read (list, deletion status), config.manage (everything else). MCP scope: manage:motor-blocks.

Credentials are never returned. Responses carry credentialsAvailable: false and a dashboard link; passwords, API-key secrets and certificates are only shown in the dashboard. The auth method is readable (activeAuthMethod) and dashboard-only to change.

Errors use a stable machine-readable code (for example motor_block_limit_reached, domain_not_verified, motor_block_name_conflict, motor_block_not_authorized, motor_block_must_be_inactive) with a human message and, where helpful, a nextAction. A request that names two different Motor Blocks (for example in the URL and in ?motorBlockId=) is refused with 400 motor_block_id_conflict. Operations that change or remove a block require confirm: true (otherwise 400 confirmation_required).

GET /account/motor-blocks​

Production blocks covered by your current grant, including inactive ones, each with its domain, activeAuthMethod and credentialsAvailable: false. Returns an empty array when the grant covers none.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/motor-blocks"

POST /account/motor-blocks​

Create a Motor Block on a verified, non-sandbox domain you own, within your plan's limit. Requires an Idempotency-Key header (UUID v4): replaying the same key with the same input returns the same block and never creates a duplicate; reusing it with different input is 409 idempotency_conflict. The new block is added to your grant in the same transaction (authorizationUpdated), so it can be used at once by its motorBlockId.

Parameters: name (required), domainId (required), type (optional, defaults to general_purpose), description (optional, up to 500 characters)

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Receipts","domainId":"$DOMAIN_ID","type":"transactional"}' \
"$BASE_URL/account/motor-blocks"

PATCH /account/motor-blocks/{id}/name​

Rename a block. Display name only — the SMTP username and all credentials are unchanged.

Parameters: name (required)

curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Billing mail"}' \
"$BASE_URL/account/motor-blocks/$BLOCK_ID/name"

PATCH /account/motor-blocks/{id}/type​

Switch a block between transactional and general_purpose. Requires confirm: true.

Parameters: type (required), confirm (required, true)

curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"general_purpose","confirm":true}' \
"$BASE_URL/account/motor-blocks/$BLOCK_ID/type"

POST /account/motor-blocks/{id}/assign-domain​

Move a block onto another verified, non-sandbox domain you own. Requires confirm: true.

Parameters: domainId (required), confirm (required, true)

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"domainId":"$DOMAIN_ID","confirm":true}' \
"$BASE_URL/account/motor-blocks/$BLOCK_ID/assign-domain"

POST /account/motor-blocks/{id}/deactivate​

Pause a block: sends are refused until it is reactivated. Reversible — nothing is deleted. Requires confirm: true. A block must be deactivated before it can be permanently deleted.

curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm":true}' \
"$BASE_URL/account/motor-blocks/$BLOCK_ID/deactivate"

POST /account/motor-blocks/{id}/reactivate​

Resume a paused block. Refused with 409 motor_block_domain_required if the block has no verified production domain, or 409 motor_block_delete_in_progress if deletion is already queued.

curl -X POST -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/motor-blocks/$BLOCK_ID/reactivate"

DELETE /account/motor-blocks/{id}​

Permanently delete a block — asynchronous and irrecoverable. The block must already be deactivated (409 motor_block_must_be_inactive). Requires confirm: true and an explicit boolean deleteHistory: true also deletes the block's message logs and events, while false is refused with 409 motor_block_history_present if any exist. Deletion is queued and returns 202 with a jobId; poll the status route below. Other blocks that share the same domain are not affected.

Parameters: confirm (required, true), deleteHistory (required, boolean)

curl -X DELETE -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm":true,"deleteHistory":false}' \
"$BASE_URL/account/motor-blocks/$BLOCK_ID"

GET /account/motor-block-deletions/{jobId}​

Status of a permanent deletion. Still available after the block itself is gone, and only visible to the account that queued it.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/motor-block-deletions/$JOB_ID"

GET /account/authorization/motor-blocks/{id}​

Infrastructure check used by the hosted MCP server to get a live yes/no on whether your current grant covers one block before it serves task or resource state for it. Returns authorized: true, or 403. It is not a customer workflow and has no MCP tool.

curl -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/account/authorization/motor-blocks/$BLOCK_ID"

Email Sending (HTTP API)​

These endpoints use a different base URL and authentication from the Public API v1 endpoints above.

Base URL: https://api.motorical.com

Authentication: Motor Block API Key via Authorization: ApiKey mk_live_... or X-Api-Key: mk_live_... header.

POST /v1/send​

Send an email via the HTTP API. Requires a Motor Block API key with a verified domain and valid DNS setup (SPF + DKIM).

curl -X POST https://api.motorical.com/v1/send \
-H "Authorization: ApiKey mk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Hello from Motorical",
"text": "Plain text body",
"html": "<p>HTML body</p>",
"dryRun": false
}'

Request Body:

FieldTypeRequiredDescription
fromstringYesBare sender address (must match motor block's verified domain). Do not put a display name here.
fromNamestringNoOptional From display name (max 78). Produces From: "Acme Billing" <sender@yourdomain.com>. Do not set a From custom header (DISALLOWED_HEADERS).
tostring or string[]YesRecipient address(es)
subjectstringYesEmail subject
textstringOne of text/htmlPlain text body
htmlstringOne of text/htmlHTML body
headersobjectNoCustom email headers (see Custom headers below)
dryRunbooleanNoValidate the send request without queueing mail or writing an email log. Defaults to false: the request is sent unless you pass dryRun: true.

Response (real send):

{
"success": true,
"data": {
"id": "3f7b1f2e-9c4a-4a1e-9f2b-7d1c8e5a0b93",
"acceptanceStatus": "queued",
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Hello from Motorical",
"timestamp": "2026-08-25T12:00:00.000Z"
},
"message": "Accepted for delivery. This is not delivery: read the outcome from the message status."
}

data.id is the send UUID — pass it to GET /messages/{id} and GET /messages/{id}/events. There is no messageId field and no status field on a real send.

Response (dryRun: true) has a different shape — no id (nothing was queued), plus extra validation metadata:

{
"success": true,
"dryRun": true,
"data": {
"status": "validated",
"acceptanceStatus": "validated",
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Dry run check",
"domain": "yourdomain.com",
"motorBlockId": "0b6c1a44-1f2d-4c8e-9a3b-5e7d2c9f1a80",
"bodySize": 42,
"hasText": true,
"hasHtml": true,
"recipientCount": 1,
"timestamp": "2026-08-25T12:00:00.000Z"
},
"message": "Email payload validated; dryRun=true so no email was queued."
}

Response (idempotent replay) — when an Idempotency-Key matches a send from the last 24 hours, the original data.id is returned with "idempotent": true at the response root and no new message is queued.

Rate Limit / Quota Errors (429): An accepted send, and a send refused for a Motor Block or account ceiling, carry RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers. They are not present on dryRun: true responses, on the rate_limited_warmup refusal (which sets only Retry-After), or when the quota check itself is unavailable (Motorical fails open rather than emit placeholder numbers) — missing headers mean "unknown," never "unlimited." A refused send returns 429 with a Retry-After header and one of rate_limited_block, rate_limited_account, or rate_limited_warmup as the error field:

{
"success": false,
"error": "rate_limited_block",
"message": "Rate limit exceeded (daily)",
"retryAfterSeconds": 33953
}

See Rate Limits & Retention for the full contract, including which of these are enforced today vs. still rolling out, and GET /account/rate-limits for checking your account-wide standing ahead of time.

Marketing blocks (403 use_comms_api): the API key belongs to a marketing motor block. Marketing blocks send only through Communications. The response is {"success": false, "error": "use_comms_api", "code": "use_comms_api", "message": "Marketing motor blocks cannot send through /v1/send or SMTP. They are sender identities for Communications: send through campaigns in the dashboard, or through the Comms Block API with your account API key and a workspace (https://docs.motorical.com/communications/overview). For transactional email use a transactional motor block."} with HTTP 403; send through campaigns in the dashboard, or through the Comms Block API with your account API key and a workspace. For transactional email use a transactional motor block.

Domain Validation: The from address domain must match the motor block's assigned domain. The domain must be verified with DKIM and SPF configured.

:::tip Friendly From vs SMTP On HTTP /v1/send, use fromName for the display name. On SMTP submission (mail.motorical.com:2587 / :2465), set the display name in the message From: header (From: "Acme Billing" <sender@yourdomain.com>) — there is no JSON fromName field on SMTP. :::

Custom headers​

The optional headers object sets additional message headers, which are applied to the outgoing message and DKIM-signed — for example In-Reply-To and References (to thread a reply into an existing conversation), Reply-To, List-Unsubscribe, or any custom X-* header.

For safety, reserved and spoofable headers are rejected with 400 (code: DISALLOWED_HEADERS):

From, Sender, Return-Path, To, Cc, Bcc, Subject, Date, Message-ID, MIME-Version, Content-*, DKIM-Signature, Received, Authentication-Results, ARC-*, and X-Motorical-*.

Use the from / fromName, to, and subject fields for those instead. Header values containing line breaks (CR/LF) are also rejected.

{
"from": "sender@yourdomain.com",
"fromName": "Acme Support",
"to": "recipient@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>"
}
}

Use dryRun: true before a production send to verify authentication, sender domain, DNS setup, recipient shape, subject, and body size without creating a queued message:

curl -X POST https://api.motorical.com/v1/send \
-H "Authorization: ApiKey mk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "sender@yourdomain.com",
"fromName": "Acme Billing",
"to": ["recipient@example.com"],
"subject": "Dry run check",
"text": "This validates only.",
"dryRun": true
}'

Dry-run responses return status: "validated" and include metadata such as sender domain, recipient count, body size, and echoed fromName. They do not include an id because nothing was queued.

GET /v1/status​

Check the API status.

curl https://api.motorical.com/v1/status

POST /v1/test-email​

Validate SMTP setup parameters for customer setup flows. This endpoint uses the same Motor Block API Key authentication as /v1/send.

curl -X POST https://api.motorical.com/v1/test-email \
-H "Authorization: ApiKey mk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"method": "starttls",
"host": "mail.motorical.com",
"port": 2587,
"username": "your_motor_block_username",
"password": "your_motor_block_password",
"to": "recipient@example.com",
"subject": "Motorical SMTP setup test",
"requireTLS": true
}'

Email Validation​

POST /api/email/validate​

Validate a single email address. No authentication required (rate limited).

Base URL: https://api.motorical.com

curl -X POST https://api.motorical.com/api/email/validate \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'

POST /api/email/validate/batch​

Validate multiple email addresses at once. Requires authentication.

curl -X POST https://api.motorical.com/api/email/validate/batch \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"emails": ["user1@example.com", "user2@example.com"]}'

Events (Server-Sent Events)​

GET /motor-blocks/{id}/events/stream​

Real-time event stream of email delivery events.

Scopes: logs.read

curl -N -H "Authorization: Bearer $TOKEN" \
"$BASE_URL/motor-blocks/$BLOCK_ID/events/stream"

The token can also be passed via ?token= or ?access_token= query parameters for browser-friendly SSE connections.

Events are delivered as SSE with types: ready, keepalive, and message. Maximum 3 concurrent SSE connections per token.