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.
https://kaimail.net/api/v1202 Accepted immediatelyEvery request must carry HTTP Basic Auth credentials:
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.
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.
You may only send from addresses you own. For every send request, the sender address must satisfy both conditions:
domain_not_owned otherwise).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.
The default way to send. You provide plain fields; the server builds the MIME message for you (including Date and Message-ID headers).
| 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).
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>"
}'
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.
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
$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"];
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.
| 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.
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 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).
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
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.
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:
failed.sent and error lists the refused recipients.failed rather than risking a duplicate — check the Logs dashboard before resending.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
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.
| 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. |
| 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 |
POST /api/v1/email/send, store the returned tracking_id.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.429 by backing off and checking GET /api/v1/usage.