Sending Email over the HTTP API


KaiMail's HTTP Sending API lets your application send email from your own custom domain over HTTPS — no SMTP library required. It is an alternative front door to the same submission pipeline used by authenticated SMTP on port 587, so DKIM signing, sender validation, quota enforcement and delivery logging behave identically no matter which channel you use. Use the HTTP API when your platform blocks outbound port 587, when you are calling from serverless environments, or when JSON over HTTPS is simply more convenient.

  • Base URL: https://kaimail.net/api/v1
  • Transport: HTTPS only
  • Format: JSON request and response bodies (UTF-8)
  • Authentication: HTTP Basic Auth
  • Model: asynchronous — sends are queued and return 202 Accepted immediately

Authentication

Every request must carry HTTP Basic Auth credentials:

  • Username — your KaiMail account email address
  • Password — your SMTP password (shown in the KaiMail dashboard)

These are the same credentials used for SMTP submission on port 587. Your account password for logging into the web dashboard will not work here.

curl -u "[email protected]:YOUR_SMTP_PASSWORD" https://kaimail.net/api/v1/usage

Missing or invalid credentials return 401 with the error code invalid_credentials. Valid credentials on a plan that does not include outbound sending return 403 with sending_not_allowed — this distinction lets your integration tell a wrong password apart from a plan limitation.

Outbound sending requires a plan with SMTP sending enabled.

Response envelope

Every response, whether success or error is a JSON object containing a request_id, a unique identifier generated per request. Include it when contacting support about a specific API call.

Success responses carry the payload under data:

{
  "request_id": "0f6d2c9be5a34f7f9f0c1d2e3a4b5c6d",
  "data": { "...": "..." }
}

Error responses carry an error object with a machine-readable code and a human-readable message:

{
  "request_id": "0f6d2c9be5a34f7f9f0c1d2e3a4b5c6d",
  "error": {
    "code": "quota_exceeded",
    "message": "Monthly sending quota reached."
  }
}

Handle errors by switching on error.code, not on the message text — messages may change.

Sender requirements

You may only send from addresses you own. For every send request, the sender address must satisfy both conditions:

  1. Its domain is one of your active custom domains registered with KaiMail (domain_not_owned otherwise).
  2. The full address is a registered active mailbox on that domain (mailbox_not_registered otherwise).

Messages are DKIM-signed with your domain's key (selector kaimail) during submission, so make sure your domain's DKIM DNS record is published as shown in the dashboard.


POST /api/v1/email/send

The default way to send. You provide plain fields; the server builds the MIME message for you (including Date and Message-ID headers).

Request body

Field Type Required Description
from string yes Sender address. A display name is allowed: "Support <[email protected]>". The address part must pass the sender requirements above.
to string or array of strings yes Recipient address(es). Maximum 100 recipients per message. All recipients appear in the message's To: header — use send-raw if you need Bcc semantics.
subject string no Message subject. Truncated to 998 characters.
text string see note Plain-text body.
html string see note HTML body.

At least one of text or html is required. If both are given, the message is sent as multipart/alternative (text + HTML).

Example

curl -X POST https://kaimail.net/api/v1/email/send \
  -u "[email protected]:YOUR_SMTP_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Support <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Welcome aboard",
    "text": "Hello — thanks for signing up.",
    "html": "<p>Hello — thanks for <strong>signing up</strong>.</p>"
  }'

Response — 202 Accepted

{
  "request_id": "0f6d2c9be5a34f7f9f0c1d2e3a4b5c6d",
  "data": {
    "tracking_id": "k2vq81xw3n7p5d09smc4",
    "status": "queued",
    "recipients": ["[email protected]"]
  }
}

202 means the message passed validation and was queued — not that it was delivered. Keep the tracking_id to poll the status endpoint below.

Python example

import requests

resp = requests.post(
    "https://kaimail.net/api/v1/email/send",
    auth=("[email protected]", "YOUR_SMTP_PASSWORD"),
    json={
        "from": "Support <[email protected]>",
        "to": ["[email protected]"],
        "subject": "Welcome aboard",
        "text": "Hello — thanks for signing up.",
    },
    timeout=30,
)
resp.raise_for_status()
tracking_id = resp.json()["data"]["tracking_id"]

PHP example

<?php
$payload = [
    "from" => "Support <[email protected]>",
    "to" => ["[email protected]"],
    "subject" => "Welcome aboard",
    "text" => "Hello — thanks for signing up.",
];

$ch = curl_init("https://kaimail.net/api/v1/email/send");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_USERPWD => "[email protected]:YOUR_SMTP_PASSWORD",
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$body = json_decode($response, true);
if ($status !== 202) {
    throw new RuntimeException(
        "send failed: {$body['error']['code']}: {$body['error']['message']}"
    );
}
$trackingId = $body["data"]["tracking_id"];

POST /api/v1/email/send-raw

For full control over the MIME message — attachments, custom headers, Bcc, pre-built messages from an email library. You supply the complete RFC 822 message, base64-encoded, plus the SMTP envelope.

Request body

Field Type Required Description
raw_message string yes The complete RFC 822 message, base64-encoded (standard encoding, with padding).
to array of strings yes The envelope recipients (SMTP RCPT TO). Maximum 100. Delivery goes to these addresses only — the To:/Cc:/Bcc: headers inside the message are not parsed for routing. This is how you implement Bcc: include the address here but not in the headers.
envelope_from string no The envelope sender (SMTP MAIL FROM). Defaults to the address in the message's From: header. Must pass the sender requirements.

Note: base64 inflates the payload by about 33%; the plan's message-size limit applies to the decoded message bytes.

Python example

import base64
import requests
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Billing <[email protected]>"
msg["To"] = "[email protected]"
msg["Subject"] = "Your invoice"
msg.set_content("Invoice attached.")
msg.add_attachment(
    open("invoice.pdf", "rb").read(),
    maintype="application", subtype="pdf", filename="invoice.pdf",
)

resp = requests.post(
    "https://kaimail.net/api/v1/email/send-raw",
    auth=("[email protected]", "YOUR_SMTP_PASSWORD"),
    json={
        "raw_message": base64.b64encode(msg.as_bytes()).decode("ascii"),
        "to": ["[email protected]", "[email protected]"],
        "envelope_from": "[email protected]"
    },
    timeout=30,
)

PHP example

PHP has no built-in MIME builder, so this example uses PHPMailer (composer require phpmailer/phpmailer) to construct the message, then submits it through the API instead of SMTP:

<?php
require "vendor/autoload.php";

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->setFrom("[email protected]", "Billing");
$mail->addAddress("[email protected]");
$mail->Subject = "Your invoice";
$mail->Body = "Invoice attached.";
$mail->addAttachment("invoice.pdf");
$mail->preSend();                        // build the MIME message without sending
$raw = $mail->getSentMIMEMessage();

$payload = [
    "raw_message" => base64_encode($raw),
    "to" => ["[email protected]", "[email protected]"],
    "envelope_from" => "[email protected]",
];

$ch = curl_init("https://kaimail.net/api/v1/email/send-raw");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_USERPWD => "[email protected]:YOUR_SMTP_PASSWORD",
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);

The response is identical in shape to /email/send (202 with tracking_id, status, recipients).


GET /api/v1/email/<tracking_id>

Returns the send status of a previously queued message. You can only query messages sent from your own account.

curl -u "[email protected]:YOUR_SMTP_PASSWORD" \
  https://kaimail.net/api/v1/email/k2vq81xw3n7p5d09smc4

Response — 200 OK

{
  "request_id": "6b1a2c3d4e5f60718293a4b5c6d7e8f9",
  "data": {
    "tracking_id": "k2vq81xw3n7p5d09smc4",
    "status": "sent",
    "envelope_from": "[email protected]",
    "recipients": ["[email protected]"],
    "subject": "Welcome aboard",
    "created_time": "2026-07-20T04:12:33Z",
    "processed_time": "2026-07-20T04:12:41Z",
    "error": null
  }
}

Timestamps are ISO 8601 in UTC. An unknown tracking_id returns 404 with code not_found.

Status lifecycle

queued ──▶ sending ──▶ sent
                  └──▶ failed
Status Meaning
queued Accepted and waiting for submission.
sending A worker is currently submitting the message.
sent Accepted for delivery by KaiMail's mail server. Per-recipient delivery results and bounces are visible in the Logs dashboard.
failed Gave up. error contains the reason.

Notes on semantics:

  • Transient submission failures are retried automatically, up to 3 attempts with backoff. Only after that does the job become failed.
  • If some (but not all) recipients are refused at submission, the status is sent and error lists the refused recipients.
  • KaiMail never sends the same queued message twice. In the rare case a worker is interrupted mid-submission, the delivery outcome is unknown and the job is marked failed rather than risking a duplicate — check the Logs dashboard before resending.

GET /api/v1/usage

Returns your sending entitlement and remaining quota. Useful for pre-flight checks and monitoring.

curl -u "[email protected]:YOUR_SMTP_PASSWORD" https://kaimail.net/api/v1/usage

Response — 200 OK

{
  "request_id": "9c8b7a6d5e4f30211203f4e5d6c7b8a9",
  "data": {
    "smtp_sending": true,
    "sent_this_period": 42,
    "quota": 1000,
    "remaining": 958,
    "period_days": 30
  }
}
Field Description
smtp_sending Whether your plan allows outbound sending at all.
sent_this_period Emails sent (counted per recipient) in the rolling window.
quota Your plan's sending quota for the window. -1 means unlimited.
remaining Quota left. null when the quota is unlimited.
period_days Length of the rolling usage window (currently 30 days).

Quota is counted per recipient: one message to 10 recipients consumes 10 units. Messages already queued but not yet sent also count toward the quota check at request time, so a burst of queued sends cannot overshoot your limit.


Error reference

HTTP error.code Meaning / typical fix
400 invalid_json Body is not a JSON object.
400 invalid_request A field is missing or malformed (bad recipient address, missing from, too many recipients, no text/html, …). The message says which.
400 invalid_message send-raw only: raw_message is not valid base64, or decodes to an empty message.
401 invalid_credentials Wrong or missing Basic Auth credentials. Use your account email + SMTP password.
403 sending_not_allowed Credentials are valid but your plan does not include outbound sending.
403 domain_not_owned The sender's domain is not one of your active custom domains.
403 mailbox_not_registered The sender address is not a registered active mailbox.
404 not_found No message with that tracking_id on your account.
413 message_too_large Message exceeds your plan's size limit. The message states both sizes.
429 quota_exceeded Monthly sending quota reached (including queued messages). Check /usage.

Limits at a glance

Limit Value
Recipients per message 100
Subject length 998 characters
Message size Per plan (see your plan details; applies to decoded bytes for send-raw)
Sending quota Per plan, counted per recipient over a rolling 30-day window
Submission retries 3 attempts with backoff, then failed

Integration checklist

  1. Enable a plan with SMTP sending, add your custom domain, and publish the DKIM record shown in the dashboard.
  2. Register the mailbox you want to send from.
  3. Get your SMTP password from the dashboard — the API shares it with port 587.
  4. POST /api/v1/email/send, store the returned tracking_id.
  5. Poll GET /api/v1/email/<tracking_id> until sent or failed; treat sent as "handed to the mail server" and watch the Logs dashboard for bounces.
  6. Handle 429 by backing off and checking GET /api/v1/usage.