Transactional emails are the messages your product sends because a user did something: signed up, reset a password, paid an invoice, got invited to a workspace. They need to go out quickly, from your own domain, and without slowing the request that triggered them.
This article shows how to send them through the Inbixio REST API from the languages teams use most, then turns the one-off call into a small helper you can drop into any codebase.
The request, in one place
Every example below sends the same HTTP request:
- Method and URL:
POST https://backend.inbixio.com/api/send-email/, including the trailing slash - Authentication: an
X-API-Keyheader with a key that has the Send permission - Body: JSON with
to_email,subjectandmessage(HTML)
A successful call returns 200 with {"message": "Email queued successfully."}. If you have not created a key or connected an SMTP server yet, start with the five-minute quickstart.
In all examples the key is read from an environment variable called INBIXIO_API_KEY. Keep it on the server; never ship it to a browser or mobile app.
cURL
Useful for testing a key, debugging from a server, or calling from a shell script or cron job:
curl -sS -X POST https://backend.inbixio.com/api/send-email/ \
-H "X-API-Key: $INBIXIO_API_KEY" \
-H "Content-Type: application/json" \
--max-time 15 \
-d '{
"to_email": "ada@example.com",
"subject": "Your invoice is ready",
"message": "<p>Hi Ada, your invoice for October is ready.</p>"
}'
-sS hides the progress bar but still prints errors, and --max-time stops a script from hanging if the network does.
Node.js
Node.js 18 and later ship with fetch, so you do not need an HTTP library:
// send-email.js
const INBIXIO_URL = 'https://backend.inbixio.com/api/send-email/';
export async function sendEmail({ to, subject, html }) {
const res = await fetch(INBIXIO_URL, {
method: 'POST',
headers: {
'X-API-Key': process.env.INBIXIO_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to_email: to, subject, message: html }),
signal: AbortSignal.timeout(15_000),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
const reason = body.error || body.detail || res.statusText;
throw new Error(`Inbixio ${res.status}: ${reason}`);
}
return body; // { message: 'Email queued successfully.' }
}
And using it from an Express route:
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await sendEmail({
to: user.email,
subject: 'Welcome aboard',
html: `<h1>Welcome, ${escapeHtml(user.name)}!</h1><p>Your account is ready.</p>`,
});
res.status(201).json({ id: user.id });
});
Notice escapeHtml. The message field is rendered as HTML, so anything a user typed, such as their name, must be escaped before you put it in the body.
Python
With requests:
# send_email.py
import os
import requests
INBIXIO_URL = "https://backend.inbixio.com/api/send-email/"
class EmailError(Exception):
pass
def send_email(to: str, subject: str, html: str) -> dict:
res = requests.post(
INBIXIO_URL,
headers={"X-API-Key": os.environ["INBIXIO_API_KEY"]},
json={"to_email": to, "subject": subject, "message": html},
timeout=15,
)
if not res.ok:
try:
body = res.json()
except ValueError:
body = {}
reason = body.get("error") or body.get("detail") or res.reason
raise EmailError(f"Inbixio {res.status_code}: {reason}")
return res.json()
Passing json= sets the Content-Type header for you. Always set a timeout: requests waits forever by default.
In Django, call it after the database transaction commits so you never email about a row that was rolled back:
from django.db import transaction
transaction.on_commit(lambda: send_email(user.email, "Welcome!", render_welcome(user)))
PHP
<?php
function send_email(string $to, string $subject, string $html): array {
$ch = curl_init('https://backend.inbixio.com/api/send-email/');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('INBIXIO_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'to_email' => $to,
'subject' => $subject,
'message' => $html,
]),
]);
$body = json_decode(curl_exec($ch), true) ?? [];
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Inbixio $status: " . ($body['error'] ?? $body['detail'] ?? 'request failed'));
}
return $body;
}
Sending to several people
Each request delivers to one to_email. To notify a list of users, send one request per recipient. That is what you want for transactional email anyway: every user gets their own message, nobody sees anyone else’s address, and one bad address does not block the rest.
Keep concurrency modest so you do not overwhelm your own SMTP server. A small pool of 5–10 parallel requests is plenty for most apps.
Don’t send from the request path
Calling the API is fast, but it is still a network round trip. For anything that is not time-critical, put the send in a background job (BullMQ, Celery, Sidekiq, a cloud queue) so:
- the user’s page loads without waiting on email,
- a temporary error can be retried without the user noticing, and
- a sudden spike, such as a big import that triggers thousands of welcome emails, drains at a steady rate.
For OTP codes and password resets, users are actively waiting, so send right away and show a friendly error if it fails. Handling email API errors and limits explains which errors to retry and which to surface.
A checklist before you ship
- The API key comes from an environment variable or secrets manager, and the key only has the Send permission.
- Every user-supplied value in the HTML is escaped.
- Requests have a timeout of 10–15 seconds.
- Non-200 responses are logged with the response body, so
erroranddetailmessages are not lost. - Your sending domain has SPF, DKIM and DMARC set up, so the emails reach the inbox.
Send it with Inbixio
Connect your own SMTP server, create a scoped API key and send from any language with one HTTPS request.