AxeneAxene Docs
SDKs

Java SDK

Send email, manage domains, contacts, templates, and webhooks from the JVM with the official io.axene:mailer SDK.

The official Java SDK is published under the Maven coordinates io.axene:mailer on Maven Central. Source lives in the axene-sdks repository.

The SDK has a small dependency footprint (Jackson for JSON), uses the built-in java.net.http.HttpClient, and targets Java 11 and later.

Installation

Gradle (Kotlin DSL):

implementation("io.axene:mailer:0.1.0")

Maven:

<dependency>
  <groupId>io.axene</groupId>
  <artifactId>mailer</artifactId>
  <version>0.1.0</version>
</dependency>

The SDK requires Java 11 or later. It uses the JDK's built-in java.net.http.HttpClient, so there is no third-party HTTP client to configure.

Client setup

Create an API key in the dashboard under Settings -> API Keys. Keys start with axm_k_. Construct the client with that key:

import io.axene.mailer.AxeneMailerClient;
 
AxeneMailerClient axene = new AxeneMailerClient("axm_k_your_api_key");

An overloaded constructor lets you override the base URL and the retry count:

AxeneMailerClient axene = new AxeneMailerClient(
    "axm_k_your_api_key",
    "https://mail.axene.io", // base URL (a trailing slash is trimmed)
    3                         // total attempts on 429 / 5xx, including the first
);
Constructor argumentTypeDefaultDescription
apiKeyString(required)Your API key. Starts with axm_k_. Must not be blank.
baseUrlStringhttps://mail.axene.ioOverride the API base URL. A trailing slash is trimmed automatically.
maxRetriesint3Total attempts on 429 / 5xx, including the first. Clamped to at least 1.

Resources are exposed as accessor methods that share a single transport: axene.emails(), axene.domains(), axene.contacts(), axene.suppressions(), axene.templates(), and axene.webhooks().

Send an email

emails().send(...) queues a single message and returns its id and initial status. Build the message with SendEmail.builder().

import io.axene.mailer.AxeneMailerClient;
import io.axene.mailer.SendEmail;
import io.axene.mailer.SendEmailResult;
 
AxeneMailerClient axene = new AxeneMailerClient("axm_k_your_api_key");
 
SendEmailResult result = axene.emails().send(SendEmail.builder()
    .from("[email protected]", "Your Company")
    .to("[email protected]")
    .subject("Your receipt")
    .html("<h1>Thanks for your order</h1><p>Your receipt is attached.</p>")
    .text("Thanks for your order. Your receipt is attached.")
    .build());
 
System.out.println(result.id + " " + result.status);

The returned SendEmailResult has these fields:

FieldTypeDescription
idStringThe queued message id.
statusStringInitial status, typically queued.
messageIdStringThe SMTP Message-ID, wire field message_id. May be null.
rejectionReasonStringWhy the message was rejected, wire field rejection_reason. Null when accepted.

A few conveniences worth knowing:

  • The builder exposes a clean from(...) and maps it to the wire field from_ for you. You never write from_.
  • from, to, cc, bcc, and replyTo accept a bare email string, or an email plus a display name. to can be called more than once to add multiple recipients.
  • Provide html, text, or both.
  • sendAt(Instant) schedules the message for later delivery (Starter plan and up).
import java.time.Instant;
import java.time.temporal.ChronoUnit;
 
axene.emails().send(SendEmail.builder()
    .from("[email protected]")
    .to("[email protected]", "Ada")
    .to("[email protected]")
    .cc("[email protected]")
    .replyTo("[email protected]")
    .subject("Welcome aboard")
    .html("<p>Welcome!</p>")
    .tag("onboarding")
    .header("X-Campaign", "spring")
    .sendAt(Instant.now().plus(1, ChronoUnit.HOURS))
    .build());

AxeneMailerClient also provides a shortcut axene.send(email) that delegates to emails().send(email).

Emails

import io.axene.mailer.*;
import java.time.Instant;
import java.util.List;
 
// Send a single email.
SendEmailResult sent = axene.emails().send(SendEmail.builder()
    .from("[email protected]").to("[email protected]")
    .subject("Hi").html("<p>Hi</p>").build());
 
// Send a batch (bare array on the wire; Starter plan and up, capped by your plan).
BatchResult batch = axene.emails().sendBatch(List.of(
    SendEmail.builder().from("[email protected]").to("[email protected]").subject("Hi").html("<p>1</p>").build(),
    SendEmail.builder().from("[email protected]").to("[email protected]").subject("Hi").html("<p>2</p>").build()
));
 
// Dry-run a send without sending it.
ValidationResult check = axene.emails().validate(SendEmail.builder()
    .from("[email protected]").to("[email protected]").subject("Hi").html("<p>Hi</p>").build());
 
// List recent emails (newest first). page is zero-based, limit is 1-100.
List<EmailRecord> emails = axene.emails().list("delivered", 0, 20);
 
// Fetch one email with bodies and events.
EmailDetail detail = axene.emails().get(sent.id);
 
// List delivery / open / click / bounce events for an email.
List<EmailEvent> events = axene.emails().events(sent.id);
 
// Re-send a bounced, rejected, or failed email as a new message.
SendEmailResult resent = axene.emails().retry(sent.id);
 
// Search. q supports inline tokens: to:, from:, status:, domain:, tag:
List<EmailSearchHit> hits = axene.emails().search("welcome status:delivered", null, null, 0, 10);
 
// Poll for emails whose status changed at or after a timestamp (max 50 rows).
List<EmailRecord> updated = axene.emails().updates(Instant.now().minusSeconds(60));

Scheduled emails:

List<ScheduledEmail> scheduled = axene.emails().listScheduled();
axene.emails().cancelScheduled(scheduled.get(0).id);
axene.emails().sendScheduledNow(scheduled.get(0).id);

Saved searches (named filter sets stored per user) are exposed as open maps:

import java.util.List;
import java.util.Map;
 
List<Map<String, Object>> searches = axene.emails().getSavedSearches();
axene.emails().setSavedSearches(List.of(
    Map.of("name", "Bounced today", "query", "status:bounced")
));

Domains

import io.axene.mailer.*;
import java.util.List;
 
// List your sending domains and their verification status.
List<DomainRecord> domains = axene.domains().list();
 
// Register a new domain. Returns the DNS records to publish.
DomainDetailRecord domain = axene.domains().create("yourdomain.com");
 
// Fetch a domain with its DKIM selector and DNS records.
DomainDetailRecord one = axene.domains().get(domain.id);
 
// Re-check DNS and verify.
axene.domains().verify(domain.id);
 
// Live DNS health checks (DKIM, SPF, DMARC, return-path, MX).
DomainHealth health = axene.domains().health(domain.id);
 
// Diagnose configuration issues and get a health score.
DomainDiagnosis diagnosis = axene.domains().diagnose(domain.id);
 
// Rotate the DKIM key; returns the new record to publish.
DkimRotation rotation = axene.domains().rotateDkim(domain.id);
 
// Transfer the domain to another Axene account (note may be null).
DomainTransfer transfer = axene.domains().transfer(domain.id, "[email protected]", "handoff");
 
// Check availability against public DNS, or whether it already exists in your account.
DomainAvailability availability = axene.domains().checkAvailability("newdomain.com");
DomainCheck exists = axene.domains().check("yourdomain.com");
 
// Delete a domain.
axene.domains().delete(domain.id);

Contacts

Subscriber lists, their contacts, CSV imports, and templated bulk sends.

import io.axene.mailer.*;
import java.util.List;
import java.util.Map;
 
// Lists.
List<ContactList> lists = axene.contacts().listLists();
ContactList list = axene.contacts().createList(ContactListParams.builder()
    .name("Newsletter").description("Monthly news").build());
ContactListDetail listDetail = axene.contacts().getList(list.id, 0, 50);
axene.contacts().updateList(list.id, ContactListParams.builder().name("Monthly Newsletter").build());
axene.contacts().deleteList(list.id);
 
// Contacts within a list.
Contact contact = axene.contacts().addContact(list.id, ContactParams.builder()
    .email("[email protected]")
    .name("Subscriber")
    .metadata(Map.of("plan", "pro"))
    .build());
axene.contacts().removeContact(list.id, contact.id);

uploadCsv takes the raw file bytes and a filename, sent as a single multipart field named file. The email column is auto-detected; other columns become contact metadata.

import java.nio.file.Files;
import java.nio.file.Path;
 
byte[] bytes = Files.readAllBytes(Path.of("contacts.csv"));
CsvImportResult importResult = axene.contacts().uploadCsv(list.id, bytes, "contacts.csv");

bulkSend mails a templated message to every contact in a list. Subject, html, and text may use {{email}}, {{name}}, and {{metadata_key}} placeholders. The contact_list_id wire field is set from the list id automatically.

BulkSendResult result = axene.contacts().bulkSend(list.id, BulkSendParams.builder()
    .senderAddressId("sender_123")
    .subject("Hello {{name}}")
    .html("<p>Hi {{name}}, here is the latest.</p>")
    .tag("newsletter")
    .build());

Suppressions

The do-not-send list. list returns a paginated Page envelope (items, total, page, limit).

import io.axene.mailer.*;
 
Page<Suppression> page = axene.suppressions().list(0, 50, "gmail.com");
 
// Suppress an address. reason defaults to "manual" when null.
axene.suppressions().add("[email protected]", "manual");
 
// Bulk import from a file (one email per line) as a single multipart field.
byte[] bytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("suppressions.txt"));
BulkSuppressionResult bulk = axene.suppressions().bulkUpload(bytes, "suppressions.txt");
 
axene.suppressions().remove("suppression_id");

Templates

Reusable email templates (Starter plan and up). html maps to the wire field html_body and text to text_body for you.

import io.axene.mailer.*;
import java.util.List;
 
List<Template> templates = axene.templates().list();
 
Template template = axene.templates().create(TemplateParams.builder()
    .name("Receipt")
    .subject("Your receipt")
    .html("<h1>Thanks, {{name}}</h1>")
    .text("Thanks, {{name}}")
    .build());
 
Template fetched = axene.templates().get(template.id);
axene.templates().update(template.id, TemplateParams.builder().subject("Your order receipt").build());
Template copy = axene.templates().duplicate(template.id);
axene.templates().delete(template.id);

A template's variables are derived server-side from the {{name}} placeholders in its bodies, so you do not pass them when creating or updating.

Webhooks

import io.axene.mailer.*;
import java.util.List;
 
List<Webhook> webhooks = axene.webhooks().list();
 
// Create one. The signing secret is generated and returned in plaintext.
Webhook webhook = axene.webhooks().create(WebhookParams.builder()
    .url("https://example.com/hooks/axene")
    .event("email.delivered")
    .event("email.bounced")
    .build());
 
axene.webhooks().update(webhook.id, WebhookParams.builder()
    .events(List.of("email.opened"))
    .isActive(true)
    .build());
 
// Queue a sample email.delivered delivery to test the endpoint.
WebhookTestResult test = axene.webhooks().test(webhook.id);
 
// Inspect delivery attempts (paginated envelope).
Page<WebhookDelivery> deliveries = axene.webhooks().listDeliveries(webhook.id, 0, 20, null);
WebhookDeliveryDetail delivery = axene.webhooks().getDelivery(webhook.id, deliveries.items.get(0).id);
 
axene.webhooks().delete(webhook.id);

Error handling

Every non-2xx response, and any transport failure that survives all retries, throws an AxeneException (an unchecked RuntimeException). Inspect getStatus() and getCode() to branch on specific failures.

import io.axene.mailer.AxeneException;
import io.axene.mailer.SendEmail;
 
try {
    axene.emails().send(SendEmail.builder()
        .from("[email protected]")
        .to("[email protected]")
        .subject("Hello")
        .html("<p>Hi</p>")
        .build());
} catch (AxeneException e) {
    System.err.println(e.getStatus());  // HTTP status; 0 means a transport/network failure
    System.err.println(e.getCode());    // machine-readable code from the API body, when present
    System.err.println(e.getMessage());
}

Configuration

  • Retries. Requests that return 429 or 5xx are retried with exponential backoff, honouring the Retry-After header when present. Control the total attempt count (including the first) with the maxRetries constructor argument (default 3). Multipart uploads (uploadCsv, bulkUpload) are not retried because they are not idempotent.
  • Timeout. Each request has a 30-second timeout and a 15-second connect timeout. A timeout is treated as a transport failure and is retried while attempts remain.
  • Custom base URL. Point the client at a staging or self-hosted host with the second constructor argument. A trailing slash is trimmed automatically.
AxeneMailerClient axene = new AxeneMailerClient(
    System.getenv("AXENE_API_KEY"),
    "https://staging-mail.axene.io",
    5
);

Next steps

On this page