package main
import (
"context"
"fmt"
"log"
"os"
"github.com/AhaSend/ahasend-go"
"github.com/AhaSend/ahasend-go/api"
"github.com/AhaSend/ahasend-go/models/common"
"github.com/AhaSend/ahasend-go/models/requests"
"github.com/google/uuid"
)
func main() {
client := api.NewAPIClient(api.WithAPIKey(os.Getenv("AHASEND_API_KEY")))
accountID := uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))
ctx := context.Background()
// The SDK sends a fresh Idempotency-Key with every POST and reuses it on its
// own retries. Deriving the key from your own ID instead also makes a rerun
// of the same job (after a crash, say) return the first send, not a second.
orderID := "1234"
response, _, err := client.MessagesAPI.CreateMessage(ctx, accountID, requests.CreateMessageRequest{
From: common.SenderAddress{
Email: "info@example.com",
Name: ahasend.String("Example Corp."),
},
Recipients: []common.Recipient{
{
Email: "john@example.com",
Name: ahasend.String("John Smith"),
},
},
Subject: "Hello",
TextContent: ahasend.String("Hello world!"),
Sandbox: ahasend.Bool(true),
}, api.WithIdempotencyKey("order-"+orderID+"-receipt"))
if err != nil {
log.Fatalf("Error sending message: %v", err)
}
// Each recipient gets its own message, or an error saying why it was not sent.
for _, message := range response.Data {
if message.ID != nil {
fmt.Printf("%s: %s\n", message.Recipient.Email, *message.ID)
} else if message.Error != nil {
fmt.Printf("%s: %s\n", message.Recipient.Email, *message.Error)
}
}
}import { AhaSendClient } from "@ahasend/sdk";
const client = AhaSendClient.fromEnv();
// The SDK sends a fresh Idempotency-Key automatically and reuses it on its own retries.
const result = await client.messages.send({
from: { email: "sender@example.com", name: "Example" },
recipients: [{ email: "recipient@example.net" }],
subject: "Hello from AhaSend",
html_content: "<p>Hello!</p>",
sandbox: true,
});
console.log("Sandbox message accepted.", { count: result.data.length });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"recipients": [
{
"email": "user@example.com",
"name": "John Doe"
}
],
"subject": "Welcome to Example Corp",
"html_content": "<h1>Welcome {{first_name}}!</h1>",
"text_content": "Welcome {{first_name}}!",
"substitutions": {
"first_name": "John"
}
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/messages"
payload = {
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"recipients": [
{
"email": "user@example.com",
"name": "John Doe"
}
],
"subject": "Welcome to Example Corp",
"html_content": "<h1>Welcome {{first_name}}!</h1>",
"text_content": "Welcome {{first_name}}!",
"substitutions": { "first_name": "John" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ahasend.com/v2/accounts/{account_id}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'from' => [
'email' => 'noreply@example.com',
'name' => 'Example Corp'
],
'recipients' => [
[
'email' => 'user@example.com',
'name' => 'John Doe'
]
],
'subject' => 'Welcome to Example Corp',
'html_content' => '<h1>Welcome {{first_name}}!</h1>',
'text_content' => 'Welcome {{first_name}}!',
'substitutions' => [
'first_name' => 'John'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.ahasend.com/v2/accounts/{account_id}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"from\": {\n \"email\": \"noreply@example.com\",\n \"name\": \"Example Corp\"\n },\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\"\n }\n ],\n \"subject\": \"Welcome to Example Corp\",\n \"html_content\": \"<h1>Welcome {{first_name}}!</h1>\",\n \"text_content\": \"Welcome {{first_name}}!\",\n \"substitutions\": {\n \"first_name\": \"John\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"from\": {\n \"email\": \"noreply@example.com\",\n \"name\": \"Example Corp\"\n },\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\"\n }\n ],\n \"subject\": \"Welcome to Example Corp\",\n \"html_content\": \"<h1>Welcome {{first_name}}!</h1>\",\n \"text_content\": \"Welcome {{first_name}}!\",\n \"substitutions\": {\n \"first_name\": \"John\"\n }\n}"
response = http.request(request)
puts response.read_body{
"object": "list",
"data": [
{
"object": "message",
"id": "0199f1c2-7d3a-7b4e-9a1f-2c3d4e5f6a7b",
"recipient": {
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
},
"status": "queued",
"error": "<string>",
"message_id_header": "<0199f1c2-7d3a-7b4e-9a1f-2c3d4e5f6a7b@example.com>",
"schedule": {
"first_attempt": "2023-11-07T05:31:56Z",
"expires": "2023-11-07T05:31:56Z"
}
}
]
}{
"message": "john@example.com: the message could not be rendered: unknown function: nosuch"
}{
"message": "Error message"
}{
"message": "Sending from domain 'example.com' is paused. Check your dashboard for the reason."
}{
"message": "A request with this idempotency key is already in progress"
}{
"message": "idempotency key was already used with a different request payload"
}{
"message": "Error message"
}{
"message": "the message could not be checked right now; try again in a moment"
}Send an Email via the API
Send transactional email with the AhaSend API v2: one POST request to up to 100 recipients with templates, attachments, tracking, tags and scheduled sending.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/AhaSend/ahasend-go"
"github.com/AhaSend/ahasend-go/api"
"github.com/AhaSend/ahasend-go/models/common"
"github.com/AhaSend/ahasend-go/models/requests"
"github.com/google/uuid"
)
func main() {
client := api.NewAPIClient(api.WithAPIKey(os.Getenv("AHASEND_API_KEY")))
accountID := uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))
ctx := context.Background()
// The SDK sends a fresh Idempotency-Key with every POST and reuses it on its
// own retries. Deriving the key from your own ID instead also makes a rerun
// of the same job (after a crash, say) return the first send, not a second.
orderID := "1234"
response, _, err := client.MessagesAPI.CreateMessage(ctx, accountID, requests.CreateMessageRequest{
From: common.SenderAddress{
Email: "info@example.com",
Name: ahasend.String("Example Corp."),
},
Recipients: []common.Recipient{
{
Email: "john@example.com",
Name: ahasend.String("John Smith"),
},
},
Subject: "Hello",
TextContent: ahasend.String("Hello world!"),
Sandbox: ahasend.Bool(true),
}, api.WithIdempotencyKey("order-"+orderID+"-receipt"))
if err != nil {
log.Fatalf("Error sending message: %v", err)
}
// Each recipient gets its own message, or an error saying why it was not sent.
for _, message := range response.Data {
if message.ID != nil {
fmt.Printf("%s: %s\n", message.Recipient.Email, *message.ID)
} else if message.Error != nil {
fmt.Printf("%s: %s\n", message.Recipient.Email, *message.Error)
}
}
}import { AhaSendClient } from "@ahasend/sdk";
const client = AhaSendClient.fromEnv();
// The SDK sends a fresh Idempotency-Key automatically and reuses it on its own retries.
const result = await client.messages.send({
from: { email: "sender@example.com", name: "Example" },
recipients: [{ email: "recipient@example.net" }],
subject: "Hello from AhaSend",
html_content: "<p>Hello!</p>",
sandbox: true,
});
console.log("Sandbox message accepted.", { count: result.data.length });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"recipients": [
{
"email": "user@example.com",
"name": "John Doe"
}
],
"subject": "Welcome to Example Corp",
"html_content": "<h1>Welcome {{first_name}}!</h1>",
"text_content": "Welcome {{first_name}}!",
"substitutions": {
"first_name": "John"
}
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/messages"
payload = {
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"recipients": [
{
"email": "user@example.com",
"name": "John Doe"
}
],
"subject": "Welcome to Example Corp",
"html_content": "<h1>Welcome {{first_name}}!</h1>",
"text_content": "Welcome {{first_name}}!",
"substitutions": { "first_name": "John" }
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ahasend.com/v2/accounts/{account_id}/messages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'from' => [
'email' => 'noreply@example.com',
'name' => 'Example Corp'
],
'recipients' => [
[
'email' => 'user@example.com',
'name' => 'John Doe'
]
],
'subject' => 'Welcome to Example Corp',
'html_content' => '<h1>Welcome {{first_name}}!</h1>',
'text_content' => 'Welcome {{first_name}}!',
'substitutions' => [
'first_name' => 'John'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.ahasend.com/v2/accounts/{account_id}/messages")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"from\": {\n \"email\": \"noreply@example.com\",\n \"name\": \"Example Corp\"\n },\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\"\n }\n ],\n \"subject\": \"Welcome to Example Corp\",\n \"html_content\": \"<h1>Welcome {{first_name}}!</h1>\",\n \"text_content\": \"Welcome {{first_name}}!\",\n \"substitutions\": {\n \"first_name\": \"John\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/messages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"from\": {\n \"email\": \"noreply@example.com\",\n \"name\": \"Example Corp\"\n },\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\"\n }\n ],\n \"subject\": \"Welcome to Example Corp\",\n \"html_content\": \"<h1>Welcome {{first_name}}!</h1>\",\n \"text_content\": \"Welcome {{first_name}}!\",\n \"substitutions\": {\n \"first_name\": \"John\"\n }\n}"
response = http.request(request)
puts response.read_body{
"object": "list",
"data": [
{
"object": "message",
"id": "0199f1c2-7d3a-7b4e-9a1f-2c3d4e5f6a7b",
"recipient": {
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
},
"status": "queued",
"error": "<string>",
"message_id_header": "<0199f1c2-7d3a-7b4e-9a1f-2c3d4e5f6a7b@example.com>",
"schedule": {
"first_attempt": "2023-11-07T05:31:56Z",
"expires": "2023-11-07T05:31:56Z"
}
}
]
}{
"message": "john@example.com: the message could not be rendered: unknown function: nosuch"
}{
"message": "Error message"
}{
"message": "Sending from domain 'example.com' is paused. Check your dashboard for the reason."
}{
"message": "A request with this idempotency key is already in progress"
}{
"message": "idempotency key was already used with a different request payload"
}{
"message": "Error message"
}{
"message": "the message could not be checked right now; try again in a moment"
}- Either
text_contentorhtml_contentis required from.emailmust be from a domain you own with valid DNS recordsretention.metadatamust be between 1 and 30 daysretention.datamust be between 0 and 30 days- If
reply_tois provided, do not includereply-toin headers message-idheader will be ignored and automatically generated- Schedule times must be in RFC3339 format
schedule.first_attemptmust be in the future and within 7 days of the requestschedule.expiresmust be in the future and within 8 days of the request
from.email: either
messages:send:all or the matching domain-specific send role is
sufficient.
Template check:
Every message is checked with the template engine before it is sent. Content that the check refuses is answered with 400, and nothing of the request is sent. When the check cannot run right now, the API answers 503 with a Retry-After header: send the same request again after that delay. See API errors.
Related Guides
- Before sending: verify your sender domain.
- API authentication, Permission scopes, Sending guide.
- Message CLI, SMTP settings, Node.js SDK.
- Go SDK, API errors, API rate limits.
- Retention rules.
Authorizations
Headers
Optional idempotency key for safe request retries. Must be a unique string for each logical request.
An identical request with a completed stored outcome returns the original status and body. An in-progress
execution returns 409, a changed method/path/body returns 422, and a released 5xx execution may run again.
Keys for non-secret responses expire after 24 hours. API-key create responses include a one-time secret_key,
so successful encrypted replay responses for those operations expire after 5 minutes.
255Body
Email subject line.
A {{ variable }} in a subject resolves, whatever else the subject contains -- an emoji or an accented character included -- for every recipient whose message carries substitutions, the request's own substitutions or that recipient's. A recipient with neither is sent nothing to render, so their subject arrives exactly as written, braces and all.
Pad the braces with ordinary spaces: a variable written with non-ASCII whitespace inside them is not reliably recognised.
On a subject that is being resolved and carries an emoji or an accented character, template syntax other than a variable -- {% ... %}, {# ... #} or an unclosed brace -- is rejected. A substitution value is rejected for any of {{, {% or {#, a {{ ... }} included, and so is a subject whose values combine into one of them.
Plain text content. Required if html_content is empty

