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()
listID := uuid.MustParse("22222222-2222-4222-8222-222222222222")
templateID := uuid.MustParse("33333333-3333-4333-8333-333333333333")
// The campaign copies the campaign template's design; later changes to
// the template do not reach it. Every field but Name can be set later.
response, _, err := client.CampaignsAPI.CreateCampaign(ctx, accountID, requests.CreateCampaignRequest{
Name: "October newsletter",
Subject: ahasend.String("Our news for October, {{ first_name }}"),
Preheader: ahasend.String("Three things we shipped this month"),
From: &common.SenderAddress{
Email: "news@yourdomain.com",
Name: ahasend.String("Example"),
},
ListIDs: []uuid.UUID{listID},
TemplateID: &templateID,
})
if err != nil {
log.Fatalf("Error creating campaign: %v", err)
}
fmt.Printf("Created campaign %s, ready: %t\n", response.ID, response.Ready != nil && *response.Ready)
}
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 campaign = await client.campaigns.create({
name: "October newsletter",
subject: "What is new in October",
preheader: "Three things we shipped this month",
from: { email: "news@example.com", name: "Example" },
list_ids: ["00000000-0000-4000-8000-000000000010"],
template_id: "00000000-0000-4000-8000-000000000013",
});
console.log("Campaign created.", { id: campaign.id, ready: campaign.ready });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/campaigns \
--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": "jsmith@example.com",
"list_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": true
},
"template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/campaigns"
payload = {
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": "jsmith@example.com",
"list_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": True
},
"template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
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}/campaigns",
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([
'name' => '<string>',
'subject' => '<string>',
'preheader' => '<string>',
'from' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'reply_to' => 'jsmith@example.com',
'list_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'content' => [
'mjml' => '<string>',
'html' => '<string>',
'text' => '<string>',
'text_is_custom' => true
],
'template_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
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}/campaigns")
.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\": \"jsmith@example.com\",\n \"list_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"template_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/campaigns")
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 \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": \"jsmith@example.com\",\n \"list_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"template_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"object": "campaign",
"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>",
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"reply_to": "jsmith@example.com",
"status": "draft",
"status_reason": "<string>",
"send_at": "2023-11-07T05:31:56Z",
"send_resumes_at": "2023-11-07T05:31:56Z",
"finished_at": "2023-11-07T05:31:56Z",
"list_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"editor": "advanced",
"content": {
"text_is_custom": true,
"mjml": "<string>",
"html": "<string>",
"text": "<string>"
},
"ready": true,
"issues": [
{
"code": "sender_missing",
"message": "<string>"
}
]
}{
"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"
}Create a Campaign via the API
Create a draft AhaSend marketing campaign with its sender, lists, subject, preview text and design, from MJML, HTML or a campaign template.
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()
listID := uuid.MustParse("22222222-2222-4222-8222-222222222222")
templateID := uuid.MustParse("33333333-3333-4333-8333-333333333333")
// The campaign copies the campaign template's design; later changes to
// the template do not reach it. Every field but Name can be set later.
response, _, err := client.CampaignsAPI.CreateCampaign(ctx, accountID, requests.CreateCampaignRequest{
Name: "October newsletter",
Subject: ahasend.String("Our news for October, {{ first_name }}"),
Preheader: ahasend.String("Three things we shipped this month"),
From: &common.SenderAddress{
Email: "news@yourdomain.com",
Name: ahasend.String("Example"),
},
ListIDs: []uuid.UUID{listID},
TemplateID: &templateID,
})
if err != nil {
log.Fatalf("Error creating campaign: %v", err)
}
fmt.Printf("Created campaign %s, ready: %t\n", response.ID, response.Ready != nil && *response.Ready)
}
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 campaign = await client.campaigns.create({
name: "October newsletter",
subject: "What is new in October",
preheader: "Three things we shipped this month",
from: { email: "news@example.com", name: "Example" },
list_ids: ["00000000-0000-4000-8000-000000000010"],
template_id: "00000000-0000-4000-8000-000000000013",
});
console.log("Campaign created.", { id: campaign.id, ready: campaign.ready });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/campaigns \
--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": "jsmith@example.com",
"list_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": true
},
"template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/campaigns"
payload = {
"name": "<string>",
"subject": "<string>",
"preheader": "<string>",
"from": {
"email": "jsmith@example.com",
"name": "<string>"
},
"reply_to": "jsmith@example.com",
"list_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"content": {
"mjml": "<string>",
"html": "<string>",
"text": "<string>",
"text_is_custom": True
},
"template_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
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}/campaigns",
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([
'name' => '<string>',
'subject' => '<string>',
'preheader' => '<string>',
'from' => [
'email' => 'jsmith@example.com',
'name' => '<string>'
],
'reply_to' => 'jsmith@example.com',
'list_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'content' => [
'mjml' => '<string>',
'html' => '<string>',
'text' => '<string>',
'text_is_custom' => true
],
'template_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a'
]),
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}/campaigns")
.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\": \"jsmith@example.com\",\n \"list_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"template_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/campaigns")
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 \"name\": \"<string>\",\n \"subject\": \"<string>\",\n \"preheader\": \"<string>\",\n \"from\": {\n \"email\": \"jsmith@example.com\",\n \"name\": \"<string>\"\n },\n \"reply_to\": \"jsmith@example.com\",\n \"list_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"content\": {\n \"mjml\": \"<string>\",\n \"html\": \"<string>\",\n \"text\": \"<string>\",\n \"text_is_custom\": true\n },\n \"template_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n}"
response = http.request(request)
puts response.read_body{
"object": "campaign",
"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>",
"from": {
"email": "noreply@example.com",
"name": "Example Corp"
},
"reply_to": "jsmith@example.com",
"status": "draft",
"status_reason": "<string>",
"send_at": "2023-11-07T05:31:56Z",
"send_resumes_at": "2023-11-07T05:31:56Z",
"finished_at": "2023-11-07T05:31:56Z",
"list_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"editor": "advanced",
"content": {
"text_is_custom": true,
"mjml": "<string>",
"html": "<string>",
"text": "<string>"
},
"ready": true,
"issues": [
{
"code": "sender_missing",
"message": "<string>"
}
]
}{
"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"
}campaigns:write.
Only name is required. You can set all other fields in this request, or later with Update Campaign. The response gives ready and issues: while the campaign has issues, it cannot be sent. See Issues.
Design:
The design comes from one source:
content.mjmlcreates anadvanceddesign, andcontent.htmlcreates anhtmldesign. Withcontentthat has neither, seteditor. Aneditorthat does not agree with the content is refused.editorwithoutcontentcreates an empty design, which you can complete in the dashboard’s editor.template_idcopies the design of a campaign template. The copy belongs to the campaign: later changes to the campaign template do not change it. A campaign template with no HTML is refused. Atemplate_idthat names no campaign template of your account is refused with400campaign template not found.template_idcannot go witheditororcontent.- Without any of them, the campaign has no design yet.
- 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.
- The HTML must have an unsubscribe link before you can send the campaign. See Unsubscribe link.
from.emailmust be on a domain of your account with valid DNS records. A paused domain is accepted here, but Send Campaign and Test Campaign refuse it.reply_tois an address only, without a display name.- In
subjectandpreheader, line breaks become spaces, and spaces at the start and end are removed. An empty subject or preview text counts as none. - The API checks the subject, the preview text, the sender and the reply-to by the same rules as the dashboard, and its error messages use the same words.
list_idsholds 1 to 50 lists of your account, in the order you give. A list that you give twice counts once. A list that does not exist is refused with400. To find the ID of a list, use List Lists, or see where to find a list’s ID in the dashboard.
Idempotency-Key header. See Idempotency.
Related Guides
- API authentication, Permission scopes, Idempotency.
- Campaigns API overview, Test Campaign, Send Campaign, 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
A new draft campaign. Every field but name can be set later with
Update Campaign. The design comes from at most one source: an
editor, content, both, or a template_id. With none of them the
campaign has no design yet.
Campaign name. Spaces around it are dropped.
1 - 255The subject line. Line breaks become spaces and spaces around it are dropped; an empty subject is no subject.
The preview text, cleaned as subject is.
The sender. email must be on a domain of this account with valid DNS. A paused domain is accepted here and refused when the campaign is sent.
Show child attributes
Show child attributes
The reply-to address, an address only, without a display name.
The lists to send to, in order. A list named twice counts once. Each must be a list of this account.
1 - 50 elementsThe editor of the design. Without content the campaign gets an empty design of this editor, which the dashboard's editor opens. With content, it is required when the content holds neither mjml nor html, and must agree with it otherwise. Not accepted with template_id, whose editor the design keeps.
advanced, simple, html The content of the design, by the rules of TemplateContentInput. A campaign has no draft, so where those rules name the draft, they mean the campaign's design. Not accepted with template_id.
Show child attributes
Show child attributes
A campaign template whose design the campaign copies. The copy is the campaign's own; later changes to the template do not reach it. A campaign template with no HTML is refused.
Response
Campaign created
A campaign as List Campaigns returns it, without its lists, content and issues.
Object type identifier
campaign Campaign ID
When the campaign was created
When the campaign's own fields last changed
Campaign name, as it appears in the dashboard. Recipients do not see it.
The subject line. Null until one is set.
The preview text, which most inboxes show after the subject. Null until one is set.
The sender. Null until a complete sender is set, and when its domain was deleted. name is an empty string when the campaign has no sender name, so the object can be sent back as it is.
Show child attributes
Show child attributes
{
"email": "noreply@example.com",
"name": "Example Corp"
}
The reply-to address. Null when none is set.
Where the campaign is in its life. Only a draft can be changed or
deleted.
draft: being prepared.validating: scheduled, and its lists are being checked.waiting_for_review: held for a review before it sends.scheduled: accepted, waiting for its send time.sending: sending now.paused: sending stopped for now;status_reasonsays why.sent: every message was sent.canceled: stopped before it finished;status_reasonsays why.failed: could not be sent;status_reasonsays why.
draft, validating, waiting_for_review, scheduled, sending, paused, sent, canceled, failed Why the campaign is in its status, when there is a reason to give. Null otherwise.
When the campaign sends or sent, in UTC. Null until it is scheduled.
When a send that reached the account's daily marketing limit continues, in UTC. Null otherwise.
When the send ended, in UTC. Null until then.
The lists the campaign sends to, in the order they were saved. A list that was deleted is left out.
The editor of the campaign's design. Null while the campaign has no design.
advanced, simple, html The content of the campaign's design. Null while it has no design, or its design has neither HTML nor text.
Show child attributes
Show child attributes
Whether the draft has no issue, so it can be sent. Null unless the campaign is a draft.
The reasons the draft cannot be sent yet, in a fixed order; empty when it is ready. Null unless the campaign is a draft.
Show child attributes
Show child attributes

