Ship transactional email in an afternoon
One endpoint, one API key, one POST. There is no SDK to learn and no message queue to run, so you can send a password reset on the day you decide to, not the quarter after.
For developers and programmers
Transactional email stops being a side project. Send login codes, receipts and alerts from the same codebase that handles the rest of the request, and pay for the messages you actually send.
Your addressnotifications@yourapp.com
01What changes for you
A delayed login code or missing receipt becomes your support problem. Other providers start at $20/month before you send one message.
One endpoint, one API key, one POST. There is no SDK to learn and no message queue to run, so you can send a password reset on the day you decide to, not the quarter after.
Sending is priced per 1,000 recipients, and purchased credits stay in your account until you use them. A project in testing does not carry a monthly invoice for messages it never sent.
Your key lives in your server environment or secret store, never in browser code or a public repository. Create a separate key per service and revoke it from the dashboard if it leaks.
Accepted is not delivered. Delivery activity records the result of each message so you can answer "did my customer get the receipt?" with evidence instead of a guess.
02How it works
Send from your app through HTTP or SMTP. Pay per 1,000 recipients from ₦250 / $0.50. Manage keys, domains and delivery activity in one place.
Verify the domain you send from so receiving services can identify your messages. Your dashboard shows the records to publish and the verification status.
Generate an API key in your workspace and store it in your server environment as WAZOBIA_API_KEY. Keep it out of client-side code and out of your repository.
POST to the sending endpoint with your key and the message details. The response tells you whether the message was accepted for sending, and the activity log follows it from there.
03What you get
Send from your app through HTTP or SMTP. Pay per 1,000 recipients from ₦250 / $0.50. Manage keys, domains and delivery activity in one place.
Connect your application ↗Sending keys, DNS records, mailbox credentials and delivery activity all live in one workspace, so you are not hunting across dashboards when something needs attention.
Try it in your own code
A signup needs its login code. An order needs its receipt. Send both from your backend with one HTTP request.
Use HTTP for new integrations or authenticated SMTP for tools you already use. Keep keys out of browser code and public repositories.
Follow the quickstart ↗ Test in Postman ↗A successful response means accepted for sending. Check delivery activity to see what happens next.
curl -X POST https://app.wazobiamail.com/api/v1/sending/messages \
-H 'Content-Type: application/json' \
-H "X-API-Key: $WAZOBIA_API_KEY" \
-d '{
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
}'// Node.js 18+ · run on your server
const response = await fetch('https://app.wazobiamail.com/api/v1/sending/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.WAZOBIA_API_KEY
},
body: JSON.stringify({
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
})
});
const result = await response.json();
if (!response.ok || result.success !== true) throw new Error(result.message || 'Send failed');
console.log(result);// Node.js 18+ · run on your server
const key = process.env.WAZOBIA_API_KEY;
if (!key) throw new Error('Set WAZOBIA_API_KEY');
const message = {
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
};
const response = await fetch('https://app.wazobiamail.com/api/v1/sending/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': key },
body: JSON.stringify(message)
});
const result = await response.json();
if (!response.ok || result.success !== true) throw new Error(result.message || 'Send failed');
console.log(result);<?php
// Requires the PHP cURL extension.
$key = getenv('WAZOBIA_API_KEY');
if (!$key) throw new RuntimeException('Set WAZOBIA_API_KEY');
$message = json_decode('{"to":["you@example.com"],"content":{"from":"Your Brand <hello@yourbrand.com>","subject":"Welcome to Your Brand","htmlBody":"<p>Your account is ready. Welcome aboard!</p>"}}', true, 512, JSON_THROW_ON_ERROR);
$curl = curl_init('https://app.wazobiamail.com/api/v1/sending/messages');
curl_setopt_array($curl, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . $key],
CURLOPT_POSTFIELDS => json_encode($message, JSON_THROW_ON_ERROR)
]);
$body = curl_exec($curl);
if ($body === false) throw new RuntimeException(curl_error($curl));
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
$result = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 300 || ($result['success'] ?? false) !== true) {
throw new RuntimeException($result['message'] ?? 'Send failed');
}
print_r($result);package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
func main() {
key := os.Getenv("WAZOBIA_API_KEY")
if key == "" { panic("Set WAZOBIA_API_KEY") }
payload := []byte(`{
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
}`)
req, err := http.NewRequest("POST", "https://app.wazobiamail.com/api/v1/sending/messages", bytes.NewReader(payload))
if err != nil { panic(err) }
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", key)
client := &http.Client{Timeout: 30 * time.Second}
res, err := client.Do(req)
if err != nil { panic(err) }
defer res.Body.Close()
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil { panic(err) }
if res.StatusCode >= 300 || result["success"] != true { panic(fmt.Sprint(result)) }
fmt.Println(result)
}// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use reqwest::blocking::Client;
use serde_json::{json, Value};
use std::{env, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let key = env::var("WAZOBIA_API_KEY")?;
let message = json!({
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
});
let result: Value = Client::builder().timeout(Duration::from_secs(30)).build()?
.post("https://app.wazobiamail.com/api/v1/sending/messages")
.header("X-API-Key", key)
.json(&message).send()?.error_for_status()?.json()?;
if result["success"] != true { return Err(format!("Send failed: {result}").into()); }
println!("{result}");
Ok(())
}# Python 3 · standard library only
import json
import os
import urllib.request
message = {
"to": [
"you@example.com"
],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
}
request = urllib.request.Request(
'https://app.wazobiamail.com/api/v1/sending/messages',
data=json.dumps(message).encode('utf-8'),
headers={
'Content-Type': 'application/json',
'X-API-Key': os.environ['WAZOBIA_API_KEY'],
},
method='POST',
)
# HTTP errors raise urllib.error.HTTPError.
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
if result.get('success') is not True:
raise RuntimeError(result.get('message', 'Send failed'))
print(result)// .NET 6+ · run on your server
using System.Net.Http.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key",
Environment.GetEnvironmentVariable("WAZOBIA_API_KEY"));
var response = await client.PostAsJsonAsync(
"https://app.wazobiamail.com/api/v1/sending/messages",
new {
to = new[] { "you@example.com" },
content = new {
from = "Your Brand <hello@yourbrand.com>",
subject = "Welcome to Your Brand",
htmlBody = "<p>Your account is ready. Welcome aboard!</p>"
}
});
response.EnsureSuccessStatusCode();// Java 11+ · run on your server
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var json = """
{
"to": ["you@example.com"],
"content": {
"from": "Your Brand <hello@yourbrand.com>",
"subject": "Welcome to Your Brand",
"htmlBody": "<p>Your account is ready. Welcome aboard!</p>"
}
}""";
var request = HttpRequest.newBuilder()
.uri(URI.create("https://app.wazobiamail.com/api/v1/sending/messages"))
.header("Content-Type", "application/json")
.header("X-API-Key", System.getenv("WAZOBIA_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2) throw new RuntimeException("Send failed: " + response.body());Use the HTTP sending endpoint with your API key in the request header, or connect an application that already speaks authenticated SMTP. Both routes reach the same sending infrastructure, so you can migrate between them without rewriting your message logic.
SMTP suits tools that already accept a mail host, port and credentials. The HTTP API suits codebases that already make HTTP requests from a backend. The guide on choosing between them walks through the decision in more detail.
Each plan sets hourly, daily and monthly allowances plus a per-minute rate limit. The active plan and your account status determine which limits apply, and the delivery activity view shows the outcome of every send.
Treat every send as an operation you can retry and record the message reference returned with each accepted message. That reference is what lets you reconcile activity, investigate a failed send and answer a support question with a real result.
Keep it in your server environment or a secret store. Do not put it in browser code, a public repository or a screenshot. Create a separate key for each integration when practical, and revoke a key from your dashboard if it is ever exposed.
For developers and programmers
Free sending tier available. Paid plans add hosted mailboxes and larger sending allowances.