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")
// Only the fields you set change. They go to the template's draft, the
// same draft the dashboard edits; Publish publishes the whole draft. An
// empty ReplyTo clears the reply-to address.
response, _, err := client.TemplatesAPI.UpdateTemplate(ctx, accountID, templateID, requests.UpdateTemplateRequest{
Subject: ahasend.String("Your password reset link"),
ReplyTo: &common.SenderAddress{},
Publish: true,
})
if err != nil {
log.Fatalf("Error updating template: %v", err)
}
fmt.Printf("Updated template %s, has draft: %t\n", response.Name, response.HasDraft)
}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 templateId = "00000000-0000-4000-8000-000000000009";
// Fields go to the template's draft; publish: true also publishes it.
const template = await client.templates.update(templateId, {
subject: "Welcome to the team, {{ first_name }}",
preheader: null,
publish: true,
});
console.log("Template updated.", { id: template.id, hasDraft: template.has_draft });
curl --request PUT \
--url https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": {
"email": "jsmith@example.com",
"name": "<string>"
},
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": true
},
"publish": false
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}"
payload = {
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": {
"email": "jsmith@example.com",
"name": "<string>"
},
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": True
},
"publish": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(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}/templates/{template_id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'subject' => '<string>',
'preheader' => '<string>',
'from' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'reply_to' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'content' => [
'mjml' => '<string>',
'html' => '<string>',
'text' => '<string>',
'text_is_custom' => true
],
'publish' => false
]),
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.put("https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"publish\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"publish\": false\n}"
response = http.request(request)
puts response.read_body{
"object": "template",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z",
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"variables": [
{
"name": "<string>",
"required": true
}
],
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"reply_to": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"editor": "advanced",
"has_draft": true,
"content": {
"text_is_custom": true,
"mjml": "<string>",
"html": "<string>",
"text": "<string>"
}
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "Error message"
}{
"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"
}{
"message": "Error message"
}{
"message": "template is being changed by another request, try again"
}Update a Transactional Template via the API
Change an AhaSend transactional template’s content, subject, preview text or sender. Changes go to the draft until you publish them.
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")
// Only the fields you set change. They go to the template's draft, the
// same draft the dashboard edits; Publish publishes the whole draft. An
// empty ReplyTo clears the reply-to address.
response, _, err := client.TemplatesAPI.UpdateTemplate(ctx, accountID, templateID, requests.UpdateTemplateRequest{
Subject: ahasend.String("Your password reset link"),
ReplyTo: &common.SenderAddress{},
Publish: true,
})
if err != nil {
log.Fatalf("Error updating template: %v", err)
}
fmt.Printf("Updated template %s, has draft: %t\n", response.Name, response.HasDraft)
}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 templateId = "00000000-0000-4000-8000-000000000009";
// Fields go to the template's draft; publish: true also publishes it.
const template = await client.templates.update(templateId, {
subject: "Welcome to the team, {{ first_name }}",
preheader: null,
publish: true,
});
console.log("Template updated.", { id: template.id, hasDraft: template.has_draft });
curl --request PUT \
--url https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id} \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": {
"email": "jsmith@example.com",
"name": "<string>"
},
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": true
},
"publish": false
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}"
payload = {
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": {
"email": "jsmith@example.com",
"name": "<string>"
},
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": True
},
"publish": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(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}/templates/{template_id}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'name' => '<string>',
'subject' => '<string>',
'preheader' => '<string>',
'from' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'reply_to' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'content' => [
'mjml' => '<string>',
'html' => '<string>',
'text' => '<string>',
'text_is_custom' => true
],
'publish' => false
]),
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.put("https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"publish\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/templates/{template_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"publish\": false\n}"
response = http.request(request)
puts response.read_body{
"object": "template",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z",
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"variables": [
{
"name": "<string>",
"required": true
}
],
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"reply_to": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"editor": "advanced",
"has_draft": true,
"content": {
"text_is_custom": true,
"mjml": "<string>",
"html": "<string>",
"text": "<string>"
}
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "Error message"
}{
"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"
}{
"message": "Error message"
}{
"message": "template is being changed by another request, try again"
}templates:write.
Drafts and publishing:
- Changes to the subject, preview text, sender, reply-to and content go to the template’s draft. The API and the dashboard edit the same draft. A new
nameis not drafted: it changes when the request succeeds. - Sends use the published copy until you publish the draft: pass
publish: true, or call Publish Template. - A publish publishes the whole draft, including changes made in the dashboard. A publish that its checks refuse changes nothing; Publish Template lists them. If the template changes between the draft write and the publish, and the publish then fails, the draft keeps the request’s changes and the name does not change.
- A field that you leave out does not change. The one exception is the text beside a new design: see Text.
fromreplaces the address and the name together, so afromwithoutnameclears the name. Areply_toneeds a sender, sofrom: nullalso clearsreply_to. - Each field is compared with the draft, or with the published template when there is no draft. Fields equal to those values leave the draft and the published content as they are, but
publish: truestill publishes an existing draft. A change that makes the draft equal to the published template removes the draft:has_draftbecomesfalse, and Get Template Draft answers404. - A request with no field, or with only
publish: false, is refused with400. - You can send back a non-null
contentof Get Template Draft, or of Get Template whenhas_draftisfalse. While a draft exists, Get Template answers the publishedcontent, and sending it back replaces the draft’s design and text. Leave outcontentwhen you do not change it. For asimpletemplate, send back onlycontent.text: you can change its design only in the dashboard.
- When you send a new
content.mjmlorcontent.htmland leave outcontent.text, a text written by hand stays, and a text made from the HTML is made again from the new HTML. - Send
content.textasnullto make the text again from the new HTML, also when the text was written by hand. - A text written by hand that stays must still render. If it holds a
{{,{%or{#that cannot be rendered, the request is refused with400.
- MJML must pass strict validation. For a validation error, the message gives the line of each problem.
- Images and stylesheets must use full
https://URLs, such ashttps://example.com/logo.png. The API refuses relative paths, such asimages/logo.png, andhttp:,//,data:,cid:and other schemes. A URL that starts with a template variable, such as{{ logo_url }}, is allowed. The API cannot upload files. - The MJML, the HTML and a text written by hand can each be at most 200 KB.
503 with a Retry-After header. Retry the request after that delay.
This operation accepts an Idempotency-Key header. A retry with the same key replays the first answer. A 503 stores no response, so a retry with the same key runs the request again. See Idempotency.
Related Guides
- API authentication, Permission scopes, Idempotency.
- Get Template Draft, 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.
255Body
Changes to a template. A field left out is not changed or checked,
apart from a custom text kept beside a new design.
Every field but name goes to the draft, as a dashboard save does,
and a draft equal to the published copy is not kept. The draft is
shared with the dashboard: publish: true, like Publish Template,
publishes the whole draft, including changes made in the dashboard.
name changes at once, as the last step. The editor never changes.
Template name. Spaces around it are dropped.
1 - 255The subject. Null or an empty string clears it. Line breaks become spaces and spaces around it are dropped.
998The preview text. Null or an empty string clears it. Line breaks become spaces and spaces around it are dropped.
500The default sender. It replaces the address and the name together, so a missing name clears the name; the reply-to is kept. Null clears the sender and the reply-to. email must be on a domain of this account with valid DNS records that is not paused.
Show child attributes
Show child attributes
The default reply-to address. Null clears it. It needs a sender, and its name must be empty.
Show child attributes
Show child attributes
The content of a template write. It holds at most one of mjml and
html, and at least one of mjml, html and text. The content
of an advanced or html draft, or of the Template when it has no
draft, can be sent back as it is: an mjml or html equal to the
current one is held to the size limit but not compiled again or held
to the HTML checks, and keeps the stored HTML. While a draft exists,
the Template's content is the published copy's, and sending it back
replaces the draft's design. Leave content out of a write that does
not change it. For a simple template, send back only text: its
html can be changed only in the dashboard.
The text follows these rules:
- Beside a new
mjmlorhtml, a missingtextkeeps the draft's text when it is custom, and that text must still render, as a sent text must; otherwise, and on a create, it makes the text from the new HTML. A nulltextmakes the text from the new HTML, and so does atextequal to a text made from an earlier HTML: the draft's made text, or the published copy's made text when the draft does not hold that text as custom. A readcontentcan so be sent back with a changed design. Any othertextis stored as made when it equals the text made from the new HTML, and as custom otherwise. To keep a made text beside a new design, send the design first and the text in a second request. - Otherwise a
textequal to the draft's custom text keeps it; one equal to the text made from the draft's HTML is stored as made; any other is stored as custom. A nulltextmakes the text again from the draft's HTML, and is refused when there is no HTML.
Line ends written as CRLF are stored as LF, and a blank text is
refused. mjml, html and a custom text each have a size limit.
Show child attributes
Show child attributes
Publish the draft once it is written. With only publish: true, the request publishes the draft.
Response
Template updated. The Template object is the published copy; has_draft says whether a draft is left.
Object type identifier
template Pass as template_id to Create Template Message to send this template
When the template was created
When the template's name or published content last changed
Template name, as it appears in the dashboard
The published subject, set on the template page. Empty until one is published, and overridden by a send that names its own subject.
The published preview text, injected into the delivered HTML when it is set
Every variable the design uses. A send of this template takes the
values only from each recipient's substitutions. Every recipient
must give a value for each required variable; if one does not, the
request is rejected and no message is sent.
Show child attributes
Show child attributes
The default sender, used by a send that names no from. Null when the template has none, and then every send must name one.
Show child attributes
Show child attributes
{
"email": "noreply@example.com",
"name": "Example Corp"
}
The default reply-to address, used by a send that sets no reply-to, also one that names its own from. Null when the template has none. It has the shape of a send request's reply_to; its name is always empty.
Show child attributes
Show child attributes
{
"email": "noreply@example.com",
"name": "Example Corp"
}
The editor the template is designed with. It never changes after the template is created.
advanced: designed in MJML, with the dashboard's advanced editor.simple: designed with the dashboard's simple editor. Its HTML is made from the design and can be read but not changed through the API.html: an HTML template, uploaded or written by hand.
advanced, simple, html Whether the template has a draft, changes saved in the dashboard or through the API that are not published yet. Get Template Draft returns it.
The published content. Null when the published copy has no body, as for a template that was never published. List Templates leaves this field out; every other operation that returns a Template sets it.
Show child attributes
Show child attributes

