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()
templateID := uuid.MustParse("11111111-1111-4111-8111-111111111111")
// The template supplies the body, and the subject and sender when the
// request leaves them out. Each recipient gives its own variable values.
// For a template with no default sender, set From to an address on one of
// your domains.
response, _, err := client.MessagesAPI.CreateTemplateMessage(ctx, accountID, requests.CreateTemplateMessageRequest{
TemplateID: templateID,
Recipients: []common.Recipient{
{
Email: "john@example.com",
Name: ahasend.String("John Smith"),
Substitutions: map[string]interface{}{
"first_name": "John",
"order_id": "12345",
},
},
},
Sandbox: ahasend.Bool(true),
})
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.sendTemplate({
template_id: "00000000-0000-4000-8000-000000000009",
recipients: [{ email: "recipient@example.net", substitutions: { first_name: "Ada" } }],
sandbox: true,
});
console.log("Sandbox template message accepted.", { count: result.data.length });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/messages/template \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"template_id": "0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d",
"recipients": [
{
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
}
]
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/messages/template"
payload = {
"template_id": "0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d",
"recipients": [
{
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
}
]
}
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/template",
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([
'template_id' => '0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d',
'recipients' => [
[
'email' => 'user@example.com',
'name' => 'John Doe',
'substitutions' => [
'first_name' => 'John',
'order_id' => '12345'
]
]
]
]),
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/template")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"template_id\": \"0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d\",\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\",\n \"substitutions\": {\n \"first_name\": \"John\",\n \"order_id\": \"12345\"\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/messages/template")
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 \"template_id\": \"0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d\",\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\",\n \"substitutions\": {\n \"first_name\": \"John\",\n \"order_id\": \"12345\"\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"object": "list",
"data": [
{
"object": "message",
"id": "<uuid@example.com>",
"recipient": {
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
},
"status": "queued",
"error": "<string>",
"schedule": {
"first_attempt": "2023-11-07T05:31:56Z",
"expires": "2023-11-07T05:31:56Z"
}
}
]
}{
"message": "missing required template variables: user@example.com needs first_name, order_id"
}{
"message": "Error message"
}{
"message": "Sending from domain 'example.com' is paused. Check your dashboard for the reason."
}{
"message": "Error message"
}{
"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"
}Send a Transactional Template via the API
Send a published AhaSend transactional template by its ID to up to 100 recipients. Each recipient gives its own values for the template’s variables.
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()
templateID := uuid.MustParse("11111111-1111-4111-8111-111111111111")
// The template supplies the body, and the subject and sender when the
// request leaves them out. Each recipient gives its own variable values.
// For a template with no default sender, set From to an address on one of
// your domains.
response, _, err := client.MessagesAPI.CreateTemplateMessage(ctx, accountID, requests.CreateTemplateMessageRequest{
TemplateID: templateID,
Recipients: []common.Recipient{
{
Email: "john@example.com",
Name: ahasend.String("John Smith"),
Substitutions: map[string]interface{}{
"first_name": "John",
"order_id": "12345",
},
},
},
Sandbox: ahasend.Bool(true),
})
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.sendTemplate({
template_id: "00000000-0000-4000-8000-000000000009",
recipients: [{ email: "recipient@example.net", substitutions: { first_name: "Ada" } }],
sandbox: true,
});
console.log("Sandbox template message accepted.", { count: result.data.length });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/messages/template \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"template_id": "0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d",
"recipients": [
{
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
}
]
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/messages/template"
payload = {
"template_id": "0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d",
"recipients": [
{
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
}
]
}
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/template",
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([
'template_id' => '0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d',
'recipients' => [
[
'email' => 'user@example.com',
'name' => 'John Doe',
'substitutions' => [
'first_name' => 'John',
'order_id' => '12345'
]
]
]
]),
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/template")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"template_id\": \"0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d\",\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\",\n \"substitutions\": {\n \"first_name\": \"John\",\n \"order_id\": \"12345\"\n }\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/messages/template")
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 \"template_id\": \"0195c2a4-7d3e-7b6a-9f1c-2e4d5a6b7c8d\",\n \"recipients\": [\n {\n \"email\": \"user@example.com\",\n \"name\": \"John Doe\",\n \"substitutions\": {\n \"first_name\": \"John\",\n \"order_id\": \"12345\"\n }\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"object": "list",
"data": [
{
"object": "message",
"id": "<uuid@example.com>",
"recipient": {
"email": "user@example.com",
"name": "John Doe",
"substitutions": {
"first_name": "John",
"order_id": "12345"
}
},
"status": "queued",
"error": "<string>",
"schedule": {
"first_attempt": "2023-11-07T05:31:56Z",
"expires": "2023-11-07T05:31:56Z"
}
}
]
}{
"message": "missing required template variables: user@example.com needs first_name, order_id"
}{
"message": "Error message"
}{
"message": "Sending from domain 'example.com' is paused. Check your dashboard for the reason."
}{
"message": "Error message"
}{
"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"
}from.email or the template’s default sender: either messages:send:all or the matching domain-specific send role is sufficient. Sending a template needs no templates scope. A request with no from is refused with 403 when the API key cannot send from any domain.
Template:
template_idis required. It must be the ID of one of this account’s transactional templates. List Templates returns the IDs. Any other ID, a deleted template included, is refused with404template not found.- The send uses the published copy of the template. Changes in its draft are not sent until you publish them. A template with no published content is refused with
400. - The message has the template’s HTML body with its preview text, and the template’s text body. It has no AMP part. The request cannot carry
text_content,html_contentoramp_content, and is refused with400when it does. - Get Message returns the ID of the template in the message’s
template_id.
- Leave out
fromto send from the template’s default sender. Afromin the request is used even when the template has a default sender. - When the request has no
fromand the template has no default sender, the request is refused with400from is required because the template has no default sender. - The sender, the request’s or the template’s, must be from a domain you own with valid DNS records.
- When the request has no
reply_toand noreply-toheader, the template’s reply-to is used. subjectreplaces the template’s subject. When it is left out, empty or only spaces, the template’s published subject is used. When neither has a subject, the request is refused with400.
- Each recipient’s
substitutionshold its values for the template’s variables. They are the only place for these values: asubstitutionsobject at the top of the request is refused with400. - Each recipient must give a value for each required variable of the template. The
variableslist of Get Template shows which variables are required. A variable that the design wraps in a fallback is not required. An empty value counts as a value. - When a recipient does not give a required variable, the request is refused with
400, and no message is sent. The error names each recipient and the variables it does not give, for examplemissing required template variables: user@example.com needs first_name, order_id. - A value for a name that the template does not use is accepted. Your
subjectandheadersresolve from the same values, and a{{ name }}with no value becomes empty. - In the HTML body, each value is HTML-escaped. A design that writes
{{ name|safe }}inserts that value as HTML. The subject, the headers and the text body get values as they are. - The send supplies
emailandunsubscribe_urlitself and replaces a value a recipient gives for them.emailis the recipient’s address.
unsubscribe_url, each recipient gets its own unsubscribe link. The message carries List-Unsubscribe with that link and List-Unsubscribe-Post: List-Unsubscribe=One-Click, so the unsubscribe button of a mail client works. The button suppresses the recipient for the sending domain. The link opens a confirmation page, and the recipient is suppressed after they confirm. A List-Unsubscribe or List-Unsubscribe-Post in your headers is dropped. When the design does not use unsubscribe_url, the send makes no link and sends your headers as you give them.
Other fields:
attachments, headers, tags, tracking, retention, schedule, sandbox and sandbox_result follow the rules of Create Message. The response is the same per-recipient list. When sending from the domain is paused, the request is refused with 403. A sandbox message is accepted from a paused domain.
This operation accepts an Idempotency-Key header. See Idempotency.
Related Guides
- API authentication, Permission scopes, Idempotency.
- Create Template, Sending guide, Sandbox behavior.
- API errors, API rate limits.
Authorizations
API key for authentication. Non-empty Security Requirement values are AhaSend API-key roles. Roles listed within one requirement object are jointly required; separate requirement objects are alternatives.
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.
255Path Parameters
Account ID
Body
The ID of one of this account's transactional templates. List Templates returns the IDs.
Each recipient gets a separate message. This does not set the To header to more than one address.
A recipient's substitutions hold its values for the template's variables. They are the only place for these values.
1 - 100 elementsShow child attributes
Show child attributes
The sender. Leave it out to send from the template's default sender. A from given here is used even when the template has a default sender, and it must have an email.
Show child attributes
Show child attributes
{
"email": "noreply@example.com",
"name": "Example Corp"
}
If provided, the reply-to header in headers must not be provided. When the request has neither, the template's reply-to is used.
Show child attributes
Show child attributes
{
"email": "noreply@example.com",
"name": "Example Corp"
}
Email subject line. When it is left out, empty or only spaces, the template's published subject is used.
A {{ variable }} in the subject, yours or the template's, resolves for every recipient from that recipient's substitutions and the reserved variables, whatever else the subject contains -- an emoji or an accented character included. A variable with no value becomes empty.
Pad the braces with ordinary spaces: a variable written with non-ASCII whitespace inside them is not reliably recognised.
On a subject that carries a {{ variable }} and 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.
File attachments. For an inline image, set the attachment's content_id to a value wrapped in angle brackets (e.g. <image1@example.com>). The template's HTML can then show it with cid: (e.g. <img src="cid:image1@example.com">).
Show child attributes
Show child attributes
Custom email headers. Each name must be visible ASCII with no space and no colon. reply-to header cannot be provided if reply_to is provided, message-id will be ignored and automatically generated.
A {{ variable }} in a value resolves from the recipient's substitutions and the reserved variables. On a template whose design uses unsubscribe_url, a List-Unsubscribe or List-Unsubscribe-Post header given here is dropped.
Show child attributes
Show child attributes
Tags for categorizing messages
If true, the message will be sent to the sandbox environment
The result of the sandbox send
deliver, bounce, defer, fail, suppress Tracking settings for the message, overrides default account settings
Show child attributes
Show child attributes
{ "open": true, "click": true }
Retention settings for the message, overrides default account settings
Show child attributes
Show child attributes
{ "metadata": 1, "data": 0 }
Schedule for message delivery
Show child attributes
Show child attributes

