Create email

Sends commercial email from the org through SES or SendGrid: one message per recipient, each carrying its own signed unsubscribe link in the footer and in List-Unsubscribe and List-Unsubscribe-Post (RFC 8058), and every http(s) link tagged with utm_source=email, utm_medium=<provider>, utm_campaign=<campaign>, utm_content=link<n> and from=<message id>, each only where the link does not carry it already.

POST /v1/email

Addresshttps://api.hanzo.ai/v1/email
MethodPOST
Operationpost_email
AuthAuthorization: Bearer $HANZO_API_KEY

Sends commercial email from the org through SES or SendGrid: one message per recipient, each carrying its own signed unsubscribe link in the footer and in List-Unsubscribe and List-Unsubscribe-Post (RFC 8058), and every http(s) link tagged with utm_source=email, utm_medium=<provider>, utm_campaign=<campaign>, utm_content=link<n> and from=<message id>, each only where the link does not carry it already. Every recipient is first asked of the one suppression check (marketing_clearance): an address the org's list holds, whose company unsubscribed, or that the platform blocks is not sent to, and is answered in refused with status 422 and the reason; when every recipient is refused the request is 422. A list that cannot answer sends nothing (503).

The rail is the provider the request pins (ses or sendgrid), else the org's default (PUT /v1/email/provider), else SES, else SendGrid, whichever the org holds credentials for; anything else is 400. Every message is kept in the org's ledger (GET /v1/email), counted (GET /v1/email/stats), stated on the event stream as email_sent, and put on the CRM record of the person the org holds for the address. The same idempotency key and recipient send once. Transactional mail (a code, a receipt) is not this route's: it goes by /v1/notify, which no opt-out stops.

POST /v1/email

Request

15 fields, body application/json (required).

FieldInTypeRequiredDescription
Idempotency-Keyheaderstring—IdempotencyKey names the send, so a retried request sends each recipient's message once: the same key and recipient name the same message.
bodybodystring—Body is an alias for Text for backward compatibility.
campaignbodystring—Campaign tags the messages: 1 to 128 letters, digits, '-' or '_', which is what an SES message tag may hold.
frombodystring—From is the sender address.
headersbodyobject—Headers provides optional custom MIME headers.
headers.*bodystring—
htmlbodystring—HTML is the HTML body.
idempotency_keybodystring—IdempotencyKey names the send, so a retried request sends each recipient's message once: the same key and recipient name the same message.
providerbodystring—Provider pins the rail, ses or sendgrid.
subjectbodystring—Subject is the message subject.
syncbodystring—Sync is accepted for backwards compatibility with notify sends.
template_idbodystring—TemplateID optionally references a built-in template.
template_varsbodyany—TemplateVars carries variables for template rendering.
textbodystring—Text is the plain text body.
tobodystring[]—To lists destination recipient addresses.

Response

StatusBodyMeaning
200email.Dispatchok
defaultproblem-detailsrefused

200 body — 25 fields.

FieldInTypeAlwaysDescription
messagesbodyemail.MessageRecord[]—Messages is one message per recipient that was sent, each with its own unsubscribe link; a message the provider refused is here with status failed.
messages[].bouncebodystring—Bounce is hard or soft, for a message that bounced.
messages[].bounced_atbodystring (date-time)—BouncedAt is when it bounced, and Bounce whether hard or soft.
messages[].campaignbodystring—Campaign is the campaign the message was tagged with.
messages[].clicked_atbodystring (date-time)—ClickedAt is when a link in it was first clicked.
messages[].complained_atbodystring (date-time)—ComplainedAt is when its recipient reported it as spam.
messages[].created_atbodystring (date-time)—CreatedAt is when the message was sent, or accepted to be.
messages[].delivered_atbodystring (date-time)—DeliveredAt is when the receiving server first took it.
messages[].errorbodystring—Error says why it failed, bounced, was deferred or was rejected.
messages[].frombodystring—From is the sender address, when known.
messages[].idbodystring—ID is our id for the message (msg_…): every link in it carries it as from=, and the event stream names it by it.
messages[].idempotency_keybodystring—IdempotencyKey is the key the message was sent under.
messages[].opened_atbodystring (date-time)—OpenedAt is when it was first opened.
messages[].orgbodystring—Org is the organization owning this message.
messages[].originbodystring—Origin is api for a message POST /v1/email sent, provider for one the provider's events told of, sent from the org's own tools.
messages[].providerbodystring—Provider is the rail it went by: ses or sendgrid.
messages[].provider_idbodystring—ProviderID is the provider's own id for the message, which its events name it by; empty until the provider answers one.
messages[].statusbodystring—Status is queued, sent, failed, delivered, opened, clicked, bounced or complained.
messages[].subjectbodystring—Subject is the message subject line, when known.
messages[].tobodystring[]—To lists the recipients.
messages[].unsubscribed_atbodystring (date-time)—UnsubscribedAt is when its recipient unsubscribed from it.
refusedbodyemail.Refused[]—Refused is every recipient the suppression check refused.
refused[].addressbodystring—Address is the recipient.
refused[].reasonbodystring—Reason is why: unsubscribed, company unsubscribed (the address's domain opted out of the org's mail) or blocklisted (the platform's list).
refused[].statusbodyinteger (int64)—Status is 422: the address may not be mailed.

Failure carries the platform error shape — see Errors.

Examples

hanzo email create
import { Configuration, EmailApi } from 'hanzoai';

const api = new EmailApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.postEmail({ body: "<body>", campaign: "<campaign>" });
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import EmailApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = EmailApi(client).post_email(body="<body>", campaign="<campaign>")
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.EmailAPI.PostEmail(context.Background()).Execute()
if err != nil {
	return err
}
use hanzo_client::apis::{configuration::Configuration, email_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = email_api::post_email(&cfg, Default::default()).await?;
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.EmailApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new EmailApi(client).postEmail();
curl -X POST https://api.hanzo.ai/v1/email \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "body": "<body>",
       "campaign": "<campaign>"
     }'

MCP reaches email through the email tool, which names its 8 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:

curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_emails"
         }
       }
     }'

Email API · All Hanzo APIs · Interactive reference

Was this page useful?
Broken link or missing step? Drop it in #docs on Discord or open a PR.