package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/AhaSend/ahasend-go"
"github.com/AhaSend/ahasend-go/api"
"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()
campaignID := uuid.MustParse("11111111-1111-4111-8111-111111111111")
// Leave SendAt nil to send now.
response, _, err := client.CampaignsAPI.SendCampaign(ctx, accountID, campaignID, requests.SendCampaignRequest{
SendAt: ahasend.Time(time.Now().Add(24 * time.Hour)),
})
if notReady, ok := api.AsCampaignNotReady(err); ok {
for _, issue := range notReady.Issues {
fmt.Printf("%s: %s\n", issue.Code, issue.Message)
}
os.Exit(1)
}
if err != nil {
log.Fatalf("Error sending campaign: %v", err)
}
fmt.Printf("Campaign %s is %s\n", response.ID, response.Status)
for _, warning := range response.Warnings {
fmt.Println("Warning:", warning)
}
}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 campaignId = "00000000-0000-4000-8000-000000000014";
// Leave send_at out to send now. The campaign is checked before it sends.
const campaign = await client.campaigns.send(campaignId, { send_at: "2030-01-01T09:00:00Z" });
console.log("Campaign send accepted.", { status: campaign.status, warnings: campaign.warnings });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"send_at": "2023-11-07T05:31:56Z"
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send"
payload = { "send_at": "2023-11-07T05:31:56Z" }
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/{campaign_id}/send",
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([
'send_at' => '2023-11-07T05:31:56Z'
]),
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/{campaign_id}/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"send_at\": \"2023-11-07T05:31:56Z\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send")
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 \"send_at\": \"2023-11-07T05:31:56Z\"\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>"
}
],
"warnings": [
"<string>"
]
}{
"message": "campaign is not ready to send",
"issues": [
{
"code": "subject_missing",
"message": "Add a subject before you send the campaign."
}
]
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "campaign is not a draft"
}{
"message": "idempotency key was already used with a different request payload"
}{
"message": "Error message"
}{
"message": "Error message"
}Send or Schedule a Campaign via the API
Send a draft AhaSend marketing campaign to its lists now, or schedule it for a later time. The campaign must be a draft with no issues before it can send.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/AhaSend/ahasend-go"
"github.com/AhaSend/ahasend-go/api"
"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()
campaignID := uuid.MustParse("11111111-1111-4111-8111-111111111111")
// Leave SendAt nil to send now.
response, _, err := client.CampaignsAPI.SendCampaign(ctx, accountID, campaignID, requests.SendCampaignRequest{
SendAt: ahasend.Time(time.Now().Add(24 * time.Hour)),
})
if notReady, ok := api.AsCampaignNotReady(err); ok {
for _, issue := range notReady.Issues {
fmt.Printf("%s: %s\n", issue.Code, issue.Message)
}
os.Exit(1)
}
if err != nil {
log.Fatalf("Error sending campaign: %v", err)
}
fmt.Printf("Campaign %s is %s\n", response.ID, response.Status)
for _, warning := range response.Warnings {
fmt.Println("Warning:", warning)
}
}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 campaignId = "00000000-0000-4000-8000-000000000014";
// Leave send_at out to send now. The campaign is checked before it sends.
const campaign = await client.campaigns.send(campaignId, { send_at: "2030-01-01T09:00:00Z" });
console.log("Campaign send accepted.", { status: campaign.status, warnings: campaign.warnings });
curl --request POST \
--url https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"send_at": "2023-11-07T05:31:56Z"
}
'import requests
url = "https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send"
payload = { "send_at": "2023-11-07T05:31:56Z" }
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/{campaign_id}/send",
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([
'send_at' => '2023-11-07T05:31:56Z'
]),
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/{campaign_id}/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"send_at\": \"2023-11-07T05:31:56Z\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ahasend.com/v2/accounts/{account_id}/campaigns/{campaign_id}/send")
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 \"send_at\": \"2023-11-07T05:31:56Z\"\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>"
}
],
"warnings": [
"<string>"
]
}{
"message": "campaign is not ready to send",
"issues": [
{
"code": "subject_missing",
"message": "Add a subject before you send the campaign."
}
]
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "Error message"
}{
"message": "campaign is not a draft"
}{
"message": "idempotency key was already used with a different request payload"
}{
"message": "Error message"
}{
"message": "Error message"
}send_at. Access requires campaigns:send.
Before the send:
- The campaign must be a draft. Any other status answers
409. - A campaign with issues is refused with
400campaign is not ready to send, and the response holds theissues. See Issues. - The API then checks what would stop the campaign at its send time. The domain of the sender must still be verified and not paused, your account must be able to send, and the lists must have subscribed contacts. The error message says what to fix.
- For a send at a later time, a block that a payment or the next billing period can clear does not refuse the send. The response gives it in
warnings.
- Without
send_at, the campaign is sent now. send_atis an RFC 3339 time at least 1 minute in the future.nullis refused.- To change the send time, unschedule the campaign and send it again.
- The API answers
202, and the campaign is invalidating. Its lists are checked, and the campaign can be held for a review, as for a send from the dashboard. Readstatusandstatus_reasonwith Get Campaign. See Campaign status. warningslists what the send accepted but you should know, for example that your account cannot send now while the campaign is sent later, or that the view-in-browser page cannot show the design. It is empty when there is nothing to report.
Idempotency-Key header. A refusal from this operation is stored for the key, so send a corrected request with a new key. See Idempotency and campaign retries.
Related Guides
- API authentication, Permission scopes, Idempotency.
- Test Campaign, Get Campaign Stats, 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
When to send. Without send_at the campaign is sent now.
The time to send, in RFC 3339, at least 1 minute ahead. null is refused.
Response
The send was accepted. The campaign is in validating, as the send left it.
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
Things the send accepted but you should know, such as an account that cannot send right now while the campaign is sent later, or a design its view-in-browser page cannot show. Empty when there is none.

