> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# Send Email with the REST API

> Send transactional email with AhaSend's REST API v2: curl quick start, HTML and template examples, scheduling, sandbox testing, response fields and error codes.

This guide shows how to send email through AhaSend API v2, add substitutions, attachments and schedules, and check each recipient result.

Available on Free, Pro and Max. See [plans and features](https://ahasend.com/docs/facts.md#plans-and-features).

> [!NOTE]
>
> **API v2 Features:** This guide covers the advanced API v2 create-message endpoint, which provides enhanced features like template substitutions, bulk sending, scheduled delivery, and comprehensive webhook support.

## Official SDKs

> [card] [Node.js and TypeScript SDK](https://ahasend.com/docs/guides/nodejs-sdk.md)
>
> Install @ahasend/sdk, send a sandbox email and handle each recipient result.

> [card] [Go SDK](https://ahasend.com/docs/guides/go-sdk.md)
>
> Install ahasend-go, send a sandbox email and verify webhook signatures.

Choose a [framework or platform guide](https://ahasend.com/docs/guides.md) for the full integration, or use the [CLI quickstart](https://ahasend.com/docs/cli/quickstart.md) to send from your terminal.

## What Are the API Sending Limits?

| Setting | Limit |
| - | - |
| Recipients per request | 100 |
| Request body size | 30 MB (decimal). Larger requests are rejected, not truncated |
| Resulting message size | 20 MiB (20,971,520 bytes) for the complete message: headers, body and attachments after base64 encoding, the same limit as [SMTP](https://ahasend.com/docs/smtp.md#what-are-the-smtp-limits). No separate per-attachment limit |
| API requests | 100 per second, with bursts of 200; see [rate limits](https://ahasend.com/docs/api-reference/rate-limits.md) |
| Scheduled first attempt | In the future, within 7 days |
| Scheduled expiry | After the first attempt, within 8 days |
| Retention | [Plan-specific ranges](https://ahasend.com/docs/retention.md); new-account default 7 days |

## How Do I Send an Email Through the API?

Here's a simple example to send your first email using the API:

```bash theme={null}
curl --request POST \
  --url https://api.ahasend.com/v2/accounts/{account_id}/messages \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "sandbox": true,
  "from": {
      "email": "hello@yourdomain.com",
      "name": "Your Company"
    },
    "recipients": [
      {
        "email": "user@example.com",
        "name": "John Doe"
      }
    ],
    "subject": "Welcome to our platform!",
    "text_content": "Thanks for signing up. We are excited to have you!"
  }'
```

## Which Credentials Do I Need?

All API requests require authentication using your API v2 key:

> [accordion] Authorization Header
>
> Include your API key in the Authorization header:
>
> ```text theme={null}
> Authorization: Bearer YOUR_API_KEY_HERE
> ```

> [accordion] Account ID
>
> Replace `{account_id}` in the URL with your actual account ID. Find the account ID in the dashboard account settings. Use a send-only key from **Credentials → Add → API Key v2** with `messages:send:{your-domain}`. Use `messages:send:all` when sending from many domains.
>
> ```text theme={null}
> https://api.ahasend.com/v2/accounts/{account_id}/messages
> ```

> [!NOTE]
>
> **Need API Keys?** If you haven't created API v2 credentials yet, check out our [API Credentials guide](https://ahasend.com/docs/send-api/credentials.md) for step-by-step instructions.

## Basic Examples

```json Plain Text Email theme={null}
{
  "sandbox": true,
  "from": {
    "email": "hello@yourdomain.com",
    "name": "Your Company"
  },
  "recipients": [
    {
      "email": "user@example.com",
      "name": "John Doe"
    }
  ],
  "subject": "Welcome to our platform!",
  "text_content": "Thanks for signing up. We are excited to have you!"
}
```

```json HTML Email theme={null}
{
  "sandbox": true,
  "from": {
    "email": "hello@yourdomain.com",
    "name": "Your Company"
  },
  "recipients": [
    {
      "email": "user@example.com",
      "name": "John Doe"
    }
  ],
  "subject": "Welcome to our platform!",
  "html_content": "<html><body><h1>Welcome!</h1><p>Thanks for signing up. We're <strong>excited</strong> to have you!</p><p><a href='https://yourdomain.com/get-started'>Get Started</a></p></body></html>",
  "text_content": "Welcome! Thanks for signing up. We're excited to have you! Get started at https://yourdomain.com/get-started"
}
```

```json With Template Variables theme={null}
{
  "sandbox": true,
  "from": {
    "email": "hello@yourdomain.com",
    "name": "Your Company"
  },
  "recipients": [
    {
      "email": "user@example.com",
      "name": "John Doe"
    }
  ],
  "subject": "Welcome {{first_name}}!",
  "html_content": "<h1>Welcome {{first_name}}!</h1><p>Thanks for joining {{company}}. We're excited to have you!</p>",
  "text_content": "Welcome {{first_name}}! Thanks for joining {{company}}. We're excited to have you!",
  "substitutions": {
    "first_name": "John",
    "company": "Your Company"
  }
}
```

In the HTML body, AhaSend HTML-escapes each value: a `first_name` of `<b>Ann</b>` shows as the text `<b>Ann</b>`, not as bold. To insert a value as HTML, for example a block of order rows that your code builds, write `{{ order_rows|safe }}`, and escape any user input inside that value yourself. The subject, the headers and the text body get values as they are.

## Advanced Examples

> [!NOTE]
>
> **Multiple Recipients:** When you specify multiple recipients in the recipients array, AhaSend sends separate individual emails to each recipient. This is not one email with multiple addresses in the To/CC headers, but rather individual personalized emails where each recipient only sees their own email address and can receive personalized template substitutions.

Per-recipient `substitutions` override the global `substitutions` for that recipient.

```json Bulk Email Sending theme={null}
{
  "sandbox": true,
  "from": {
    "email": "newsletter@yourdomain.com",
    "name": "Your Newsletter"
  },
  "recipients": [
    {
      "email": "user1@example.com",
      "name": "Alice Smith",
      "substitutions": {"name": "Alice"}
    },
    {
      "email": "user2@example.com",
      "name": "Bob Johnson",
      "substitutions": {"name": "Bob"}
    },
    {
      "email": "user3@example.com",
      "name": "Carol Wilson",
      "substitutions": {"name": "Carol"}
    }
  ],
  "subject": "Monthly Newsletter - {{month}}",
  "html_content": "<h1>{{month}} Newsletter</h1><p>Dear {{name}}, here's what's new this month...</p>",
  "text_content": "{{month}} Newsletter - Dear {{name}}, here's what's new this month...",
  "substitutions": {
    "month": "December",
    "name": "Valued Customer"
  }
}
```

```json Scheduled Email theme={null}
{
  "sandbox": true,
  "from": {
    "email": "reminders@yourdomain.com",
    "name": "Reminder Service"
  },
  "recipients": [
    {
      "email": "user@example.com",
      "name": "John Doe"
    }
  ],
  "subject": "Don't forget about your appointment tomorrow",
  "text_content": "Hi {{name}}, this is a friendly reminder about your appointment tomorrow at {{time}}.",
  "substitutions": {
    "name": "John",
    "time": "2:00 PM"
  },
  "schedule": {
    "first_attempt": "<RFC3339 first attempt within the next 7 days>",
    "expires": "<RFC3339 expiry after first attempt, within 8 days>"
  }
}
```

```json Transactional Email with Retention theme={null}
{
  "sandbox": true,
  "from": {
    "email": "orders@yourdomain.com",
    "name": "Your Store"
  },
  "recipients": [
    {
      "email": "customer@example.com",
      "name": "Jane Customer"
    }
  ],
  "subject": "Order #{{order_id}} Confirmation",
  "html_content": "<h1>Order Confirmation</h1><p>Thank you {{name}}! Your order #{{order_id}} for ${{total}} has been confirmed.</p><p>Tracking: {{tracking}}</p>",
  "text_content": "Thank you {{name}}! Your order #{{order_id}} for ${{total}} has been confirmed. Tracking: {{tracking}}",
  "substitutions": {
    "name": "Jane",
    "order_id": "12345",
    "total": "99.99",
    "tracking": "TRK123456789"
  },
  "retention": {
    "metadata": 30,
    "data": 7
  }
}
```

```json Testing with Sandbox Mode theme={null}
{
  "from": {
    "email": "hello@yourdomain.com",
    "name": "Your Company"
  },
  "recipients": [
    {
      "email": "test@example.com",
      "name": "Test User"
    }
  ],
  "subject": "Test Email",
  "text_content": "This is a test email sent in sandbox mode.",
  "sandbox": true,
  "sandbox_result": "deliver"
}
```

## How Do I Read the Sending Response?

The API returns a response with information about each message created:

### Successful Response (202)

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "message",
      "id": "<3f8e2b1a-7c4d-4e2a-9b1e-2f3a4b5c6d7e@yourdomain.com>",
      "recipient": {
        "email": "user@example.com",
        "name": "John Doe",
        "substitutions": {
          "first_name": "John",
          "company": "Your Company"
        }
      },
      "status": "scheduled",
      "error": null,
      "schedule": {
        "first_attempt": "<RFC3339 first attempt within the next 7 days>",
        "expires": "<RFC3339 expiry after first attempt, within 8 days>"
      }
    }
  ]
}
```

### Response Fields

> [accordion] Message Status
>
> * **`queued`** - Message accepted and queued for delivery
> * **`scheduled`** - Message scheduled for future delivery
> * **`error`** - Message failed validation or processing; `id` is `null`

> [accordion] Message ID
>
> Unique identifier for tracking the message through webhooks and logs. It is `null` when the message was not sent.

> [accordion] Schedule Information
>
> If you include the `schedule` parameter in your request, the response will include timing information including the scheduled first attempt and expiration.

## How Do I Handle a Failed Request?

HTTP `202` means the request was accepted for processing. Inspect every item's `status`; a rejected recipient has `status: "error"` and `id: null` even if the HTTP request succeeded.

| HTTP status | Meaning |
| - | - |
| 400 | Invalid JSON or request fields, or content the [template check](https://ahasend.com/docs/api-reference/errors.md#400-bad-request) cannot render |
| 401 | Missing or invalid credentials |
| 403 | Scope, account, or IP allow list prevents access |
| 404 | Account or resource was not found |
| 409 | Another request with this idempotency key is still running |
| 422 | This idempotency key was used with a different method, path or body |
| 429 | Account API request limit exceeded |
| 500 | Server error; retry with the same idempotency key |
| 503 | The template check could not run, and nothing was sent; wait for `Retry-After`, then retry with the same idempotency key |

Error responses have a human-readable `message` field. Its text can change; do not parse it as a machine error code.

```json theme={null}
{"message": "invalid request body"}
```

A completed duplicate request returns its original status and body with `Idempotent-Replayed: true`. See [idempotency](https://ahasend.com/docs/api-reference/idempotency.md) and [error handling](https://ahasend.com/docs/api-reference/errors.md).

## Validation Requirements

Based on the [API documentation](https://ahasend.com/docs/api-reference/messages/create-message.md), ensure your requests meet these requirements:

> [accordion] Content Requirements
>
> * Either `text_content` or `html_content` is required
> * Both can be provided for multipart emails
> * Use proper HTML structure for `html_content`

> [accordion] Sender Domain
>
> * `from.email` must be from a domain you own
> * Domain must have valid DNS records configured
> * Domain must be verified in your AhaSend account

> [accordion] Retention Settings
>
> * Values must be within your [plan's retention range](https://ahasend.com/docs/retention.md)
> * New accounts default to 7 days for metadata and message data
> * Zero message-data retention needs approval from AhaSend

> [accordion] Scheduling Format
>
> * Schedule times must be in RFC3339 format
> * `first_attempt` must be in the future and within 7 days of the request
> * `expires` must be after `first_attempt` and within 8 days of the request
> * Times should be in UTC timezone

## Best Practices

> [accordion] Idempotency
>
> Use [idempotency keys](https://ahasend.com/docs/api-reference/idempotency.md) for safe retries:
>
> ```bash theme={null}
> --header 'Idempotency-Key: unique-request-id-12345'
> ```
>
> Stored send results expire after 24 hours. The same key and exact request replay a stored result; a 5xx can allow re-execution. See [idempotency](https://ahasend.com/docs/api-reference/idempotency.md).

> [accordion] Template Variables
>
> AhaSend supports [MiniJinja](https://github.com/mitsuhiko/minijinja) templating language for email content. To store a design in AhaSend and send it by ID, see [Transactional templates](https://ahasend.com/docs/send-api/templates.md).
>
> * Use descriptive variable names
> * Provide fallback values in templates
> * Validate variables before sending
> * Send values as plain text: AhaSend HTML-escapes them in the HTML body. Use `{{ name|safe }}` only for HTML that your own code builds

> [accordion] Bulk Operations
>
> **Efficient bulk sending:**
>
> * Send up to 100 requests per second
> * Send up to 100 recipients per request
> * Handle partial failures gracefully
> * Implement retry logic for failed messages

> [accordion] Error Handling
>
> **Robust error handling:**
>
> * Implement exponential backoff for retries
> * Log API responses for debugging
> * Handle different status codes appropriately
> * Use [webhooks](https://ahasend.com/docs/integrations/webhooks.md) for delivery confirmation

## Related Guides

* [Domain setup](https://ahasend.com/docs/domains.md), [Send-only API keys](https://ahasend.com/docs/send-api/credentials.md), [API authentication](https://ahasend.com/docs/api-reference/authentication.md).
* [Create Message reference](https://ahasend.com/docs/api-reference/messages/create-message.md), [SMTP settings](https://ahasend.com/docs/smtp.md), [CLI sending](https://ahasend.com/docs/cli/commands/messages.md).
* [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md), [Go SDK](https://ahasend.com/docs/guides/go-sdk.md), [Sandbox mode](https://ahasend.com/docs/send-api/sandbox.md).
* [CLI webhook testing](https://ahasend.com/docs/cli/webhook-testing.md), [Permission scopes](https://ahasend.com/docs/api-reference/scopes.md), [Idempotency](https://ahasend.com/docs/api-reference/idempotency.md).
* [API errors](https://ahasend.com/docs/api-reference/errors.md), [Rate limits](https://ahasend.com/docs/api-reference/rate-limits.md), [Retention rules](https://ahasend.com/docs/retention.md).
* [Tracking setup](https://ahasend.com/docs/tracking/open-tracking.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.