Libraries and examples
There is no package to install. The API is plain HTTPS and JSON, so every language below uses its own standard HTTP client and nothing else — no dependency to add, no version to keep up with, nothing that can break when we change something.
Every example does the same thing: sends one email, passes an idempotency key so a retry is safe, and treats the response honestly.
curl
curl https://api.hamanimail.com/v1/send \
-H "Authorization: Bearer $HAMANI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Your Business <hello@yourbusiness.com.au>",
"to": "customer@example.com",
"subject": "Your receipt",
"category": "transactional",
"html": "<p>Thanks for your order.</p>",
"text": "Thanks for your order.",
"idempotencyKey": "order-10482"
}'Node.js and TypeScript
fetch is built in from Node 18. No package needed.
type SendResult = { status: "queued" | "duplicate" | "scheduled"; messageId: string };
export async function sendEmail(params: {
to: string; subject: string; html: string; text: string; idempotencyKey: string;
}): Promise<SendResult> {
const res = await fetch("https://api.hamanimail.com/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HAMANI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Your Business <hello@yourbusiness.com.au>",
category: "transactional",
...params,
}),
});
const body = await res.json();
if (!res.ok) {
// 503 and 429 are worth retrying with the SAME idempotencyKey.
const retryable = res.status === 503 || res.status === 429;
throw Object.assign(new Error(body.message ?? res.statusText), {
code: body.error, status: res.status, retryable,
});
}
return body as SendResult;
}Python
import os, requests
def send_email(to, subject, html, text, idempotency_key):
res = requests.post(
"https://api.hamanimail.com/v1/send",
headers={"Authorization": f"Bearer {os.environ['HAMANI_API_KEY']}"},
json={
"from": "Your Business <hello@yourbusiness.com.au>",
"to": to,
"subject": subject,
"category": "transactional",
"html": html,
"text": text,
"idempotencyKey": idempotency_key,
},
timeout=15,
)
body = res.json()
if not res.ok:
retryable = res.status_code in (429, 503)
raise RuntimeError(f"{body.get('error')}: {body.get('message')} (retryable={retryable})")
return bodyPHP
<?php
function send_email(array $params): array {
$payload = json_encode(array_merge([
'from' => 'Your Business <hello@yourbusiness.com.au>',
'category' => 'transactional',
], $params));
$ch = curl_init('https://api.hamanimail.com/v1/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('HAMANI_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => $payload,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$body = json_decode($raw, true);
if ($status >= 400) {
$retryable = in_array($status, [429, 503], true);
throw new RuntimeException($body['error'] . ': ' . $body['message'] . ($retryable ? ' (retryable)' : ''));
}
return $body;
}Ruby
require "net/http"
require "json"
def send_email(to:, subject:, html:, text:, idempotency_key:)
uri = URI("https://api.hamanimail.com/v1/send")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('HAMANI_API_KEY')}"
req["Content-Type"] = "application/json"
req.body = {
from: "Your Business <hello@yourbusiness.com.au>",
to: to, subject: subject, category: "transactional",
html: html, text: text, idempotencyKey: idempotency_key,
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 15) { |http| http.request(req) }
body = JSON.parse(res.body)
unless res.is_a?(Net::HTTPSuccess)
retryable = ["429", "503"].include?(res.code)
raise "#{body['error']}: #{body['message']} (retryable=#{retryable})"
end
body
endGo
package mail
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type SendResult struct {
Status string `json:"status"`
MessageID string `json:"messageId"`
}
func Send(payload map[string]any) (*SendResult, error) {
payload["from"] = "Your Business <hello@yourbusiness.com.au>"
payload["category"] = "transactional"
buf, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.hamanimail.com/v1/send", bytes.NewReader(buf))
req.Header.Set("Authorization", "Bearer "+os.Getenv("HAMANI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
var e struct{ Error, Message string }
json.NewDecoder(res.Body).Decode(&e)
retryable := res.StatusCode == 429 || res.StatusCode == 503
return nil, fmt.Errorf("%s: %s (retryable=%t)", e.Error, e.Message, retryable)
}
var out SendResult
json.NewDecoder(res.Body).Decode(&out)
return &out, nil
}What every one of these gets right
They are not decoration — each shows the three things that separate working integrations from ones that page you at 2am:
- The key comes from the environment, never a literal in the source. A key committed to a repository is a key you will be rotating in a hurry.
- An
idempotencyKeyon every send. It is what makes a retry safe. See Safe retries. 429and503are separated from the rest. Those two are worth retrying; a400or403will fail identically forever. See Errors.
Timeouts
Every example sets one — 15 seconds is a sensible default. A client with no timeout will eventually hang a worker thread waiting on a socket that is never going to answer, and that failure is far more annoying to diagnose than a clean timeout you handled.
Webhook handlers
Receiving events is the other half. Verification code in JavaScript and Python is on the Webhooks page — verify before you act on one.