# haru Mail — Full API Specification (for AI agents) This single file contains everything needed to integrate haru Mail. haru Mail is an email sending & receiving API for developers in Japan. ## Overview - Base URL: `https://harumail.app` - REST prefix: `/v1` - MCP endpoint: `POST /mcp` (Streamable HTTP, stateless; same auth as REST) - Auth header (all endpoints): `Authorization: Bearer hm_<32 hex chars>` - Content type: `application/json` (UTF-8) - Errors: always `{"error": ""}` with an appropriate HTTP status. - Rate limit: 120 requests/min per API key -> 429. - Monthly send quota per tenant; when exceeded, POST /v1/emails returns 402. ## Authentication Every API key belongs to one tenant (customer account). A key can only send from sender domains registered to its tenant. There is no way to send from an unregistered domain (403). Keys are shown once at issuance; store securely. ## Send email POST /v1/emails Request body: | field | type | required | notes | |---|---|---|---| | from | string or {email,name} | yes | `"info@example.com"` or `"Name "`. Domain must be registered to the tenant. | | to | string or array | yes | Combined to+cc+bcc <= 50 recipients. | | subject | string | yes | <= 998 chars. | | text | string | text or html required | Plain text body. | | html | string | text or html required | HTML body. Sending both is recommended. | | reply_to | string | no | Also accepts `replyTo`. | | cc, bcc | string or array | no | | | headers | object (string->string) | no | Custom headers. | | attachments | array | no | Up to 32. Each: `{filename, content (base64 string), content_type, disposition? ("attachment"|"inline"), content_id?}`. Total message size <= 5 MiB. | Success response (200): ```json { "id": "em_1a2b3c4d5e6f7a8b9c0d", "status": "sent", "from": "Name ", "to": ["customer@example.jp"], "subject": "...", "message_id": "", "created_at": "2026-08-06 12:34:56" } ``` Idempotency: pass header `Idempotency-Key: `. Retrying with the same key returns the original result (with `"idempotent": true`) and does NOT send again. Unique per tenant. Recommended for AI agents and retry loops. Failure: the attempt is still recorded in history with `status: "failed"` and the error detail; the API returns 4xx/502 with `{"error": ...}`. ## Batch send (newsletters / announcements) POST /v1/emails/batch Body: `{"emails": [, ...]}` — 1 to 50 emails per request. Each email is validated, sent, counted and recorded independently (per-recipient personalization supported). Response: `{"data": [{index, id, status} | {index, error}], "sent": n, "failed": n}` If the monthly quota is reached mid-batch, remaining items are not sent and are marked with an explanatory error. Idempotency-Key is not supported on batch. Legal note: bulk marketing email requires prior opt-in consent under Japanese law (特定電子メール法); sending unsolicited ads is prohibited by the Terms. MCP tool: `send_batch_emails` {emails: [...]}. ## Bulk / scheduled / drip campaigns (up to 1,000 per request) POST /v1/emails/bulk Body: `{"emails": [, ...], "scheduled_at"?: "YYYY-MM-DD HH:MM"}` - Up to 1,000 emails per request. Queued and sent at ~100/minute (drip, ISP-friendly). - `scheduled_at` without timezone is interpreted as JST (Asia/Tokyo); ISO8601 with TZ also accepted. Per-item `scheduled_at` overrides the request-level value. - Sender domain ownership is validated at enqueue time (fail fast). - Response: `{"batch_id": "bk_...", "queued": n, "scheduled_from": "..."}` GET /v1/bulk/:batch_id -> `{"counts": {queued, sent, failed, canceled}, "next_scheduled_at", "failures": [...]}` DELETE /v1/bulk/:batch_id -> cancels remaining queued items. Drip / step-mail pattern: enqueue the same recipient list multiple times with staggered scheduled_at values (e.g. day 0, day 3, day 7). MCP tools: `send_bulk_emails` {emails, scheduled_at?}, `get_bulk_status` {batch_id}, `cancel_bulk` {batch_id}. ## Readable email rendering MCP `get_email` / `get_inbound` return the email as clean Markdown (subject, from/to table, plain-text body with HTML stripped) — suitable for direct display in AI chat UIs. CLI: `npx harumail read ` renders the same in the terminal. Raw JSON remains available via REST GET endpoints. ## Unsubscribe / suppression (marketing email compliance) Add `"unsubscribe": true` to any send (single recipient only) for marketing/newsletter email: - Adds RFC 8058 one-click unsubscribe headers (List-Unsubscribe + List-Unsubscribe-Post, required by Gmail for bulk senders) and a footer unsubscribe link (ja). - Recipients who unsubscribe are added to the tenant's suppression list; further `unsubscribe: true` sends to them are rejected with 422. Transactional sends (without the flag) are NOT blocked by design. - Manage: GET /v1/suppressions, POST /v1/suppressions {email}, DELETE /v1/suppressions/:email ## Open/click tracking & stats Add `"track": true` (HTML emails): injects an open pixel and rewrites links to signed redirect URLs (tamper-proof, no open-redirector). First open/click timestamps are recorded. GET /v1/stats -> 30-day {sent, failed, inbound, opened, clicked, open_rate, sent_via_failover, suppressed_total}. Bulk status includes opened/clicked. MCP tools: get_stats, list_suppressions. ## Failover delivery (Business plan and above) If the primary delivery infrastructure errors (outage/rate limit), haru Mail automatically retries via a secondary provider network run by a different company (multi-provider delivery; fully server-side, zero customer setup). The `provider` field in history and `sent_via_failover` in stats show which path was used. Configuration-caused errors (unverified sender etc.) are not failed over. Enabled automatically on Business and Pro plans. ## Sent email history GET /v1/emails?limit=20&status=sent|failed -> `{"data": [{id, from, to[], subject, status, error, message_id, created_at}, ...]}` (newest first; limit 1-100, default 20) GET /v1/emails/:id -> full record including `text`, `html`, `cc`, `bcc`, `reply_to`, `attachments` (metadata only), `sent_at`. ## Inbound email (receiving) GET /v1/inbound?limit=20 -> `{"data": [{id, from_email, from_name, to_email, subject, raw_size, created_at}, ...]}` GET /v1/inbound/:id -> full record including `text`, `html`, `headers` (object). Inbound mail is retained for 90 days. Receiving requires MX setup per domain (handled by haru Mail support during onboarding). ### Inbound webhook If configured, each received email triggers: POST Headers: `content-type: application/json`, `x-harumail-signature: sha256=` Body: `{"type": "inbound.received", "data": {id, from, from_name, to, subject, created_at}}` The body does not include the message body; fetch it via GET /v1/inbound/:id. Signature = HMAC-SHA256(webhook_secret, raw request body), hex-encoded. Verify with constant-time comparison. ## Domains GET /v1/domains -> `{"data": [{domain, status, created_at}, ...], "trial_sender": "t-xxxxxx@" | null}` Domains usable in `from`. Adding domains is done by haru Mail support (SPF/DKIM/DMARC configured server-side; DKIM aligns with the from domain). Trial sender: every account gets a unique `trial_sender` address on a shared domain. You can use it as `from` immediately — no DNS or domain setup required. Exact match only (other accounts' trial addresses are rejected with 403). Switch to your own domain later by just changing `from`. ## Usage GET /v1/usage -> `{"month": "2026-08", "sent": 128, "failed": 2, "limit": 3000, "remaining": 2872}` ## Errors | HTTP | meaning | |---|---| | 400 | Malformed JSON | | 401 | Invalid/revoked API key | | 402 | Monthly quota reached | | 403 | Sender domain not registered to this tenant | | 404 | Not found | | 422 | Invalid field (message says which) | | 429 | Rate limited (120/min) | | 502 | Upstream delivery error (recorded as failed in history) | ## MCP server Endpoint: `POST /mcp` — MCP Streamable HTTP, stateless (POST only, no SSE). Auth: same `Authorization: Bearer hm_...` header. Protocol version: 2025-06-18. Register with: ``` claude mcp add --transport http harumail https://harumail.app/mcp --header "Authorization: Bearer hm_..." ``` IMPORTANT — draft-first sending (safety design): All MCP send tools (`send_email`, `send_batch_emails`, `send_bulk_emails`) only create a DRAFT and return a human-readable preview. Nothing is sent until `confirm_send` {draft_id} is called. Agents MUST show the preview to the user and call `confirm_send` ONLY after the user explicitly instructs to send. Drafts auto-expire after 24 hours. (REST /v1/emails sends directly — it is for programmatic integrations like form auto-replies where per-message confirmation is impossible.) Tools: - `send_email` {from, to, subject, text?, html?, reply_to?, cc?, bcc?} — creates a draft. - `send_batch_emails` {emails[≤50]} / `send_bulk_emails` {emails[≤1000], scheduled_at?} — draft for batches/campaigns. - `confirm_send` {draft_id} — actually sends (≤10 due-now emails: immediately; otherwise queued at 100/min). - `discard_draft` {draft_id} - `get_bulk_status` {batch_id} / `cancel_bulk` {batch_id} - `list_emails` {limit?, status?} / `get_email` {id} — get_email returns readable Markdown. - `list_inbound` {limit?} / `get_inbound` {id} — returns readable Markdown (HTML stripped). - `list_domains` {} — check usable sender domains first. - `get_usage` {} — monthly quota status. ## CLI ``` npx harumail login hm_... npx harumail send --from info@example.com --to a@b.jp --subject "..." --text "..." npx harumail logs / get / inbound / domains / usage ``` Config: `~/.config/harumail/config.json`; env vars `HARUMAIL_API_KEY`, `HARUMAIL_BASE_URL` take precedence. ## Deliverability notes - SPF, DKIM (aligned with from-domain), and DMARC are configured per sender domain by haru Mail. - Coexists with Google Workspace / existing mail providers: haru Mail only uses a dedicated subdomain (cf-bounce.) for bounce/SPF/DKIM; apex MX and SPF are never touched. - Send both `text` and `html` when possible. - `from` should be a real, monitored address (not noreply@) so replies work naturally.