Libraries and examples

Working send code in six languages, against the plain HTTPS API.

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 body

PHP

<?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
end

Go

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:

  1. 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.
  2. An idempotencyKey on every send. It is what makes a retry safe. See Safe retries.
  3. 429 and 503 are separated from the rest. Those two are worth retrying; a 400 or 403 will 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.