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

# Transactional

> Ideal for event-driven or transactional mail

The transactional sending API lets you send a single email — or a small batch — with an immediate response. Use it for password resets, order confirmations, notifications, or any message triggered by a user action.

## When to use individual sending vs. broadcasts

Use **individual sending** when you need a real-time response, want full per-message control, or are sending event-driven mail (one message per user action). Use [broadcasts](/core/sending-broadcasts) when you have a large list and want Helo to handle the delivery pipeline for you.

## Sending a message

```
POST /send/transactional
```

There are two ways to provide message content: direct fields, or a template object.

### Direct content

Use this for simple messages where the content is fully known ahead of time.

**Example request**

```bash command-line theme={null}
curl --location 'https://api.helohq.com/send/transactional' \
--header "X-Helo-Channel-Id: $HELO_CHANNEL_ID" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "from": "Acme <hello@acme.com>",
  "to": [{ "email": "jane@example.com", "name": "Jane" }],
  "subject": "Your order has shipped",
  "html": "<p>Hi Jane, your order #1234 is on its way.</p>",
  "text": "Hi Jane, your order #1234 is on its way.",
  "tags": ["transactional", "order"]
}'
```

**Response**

```json theme={null}
{
  "status": "accepted",
  "messageId": "01930f2a-4b3c-7d8e-9f0a-1b2c3d4e5f6a"
}
```

### Template content

Use this when you want to populate content dynamically using `{{variable}}` placeholders. Move your subject, HTML, and text into a `template` object alongside a `data` map.

**Example request**

```bash command-line theme={null}
curl --location 'https://api.helohq.com/send/transactional' \
--header "X-Helo-Channel-Id: $HELO_CHANNEL_ID" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "from": "Acme <hello@acme.com>",
  "to": [{ "email": "jane@example.com", "name": "Jane" }],
  "template": {
    "subject": "Hi {{firstName}}, your order has shipped",
    "html": "<p>Hi {{firstName}}, your order #{{orderId}} is on its way.</p>",
    "text": "Hi {{firstName}}, your order #{{orderId}} is on its way.",
    "data": {
      "firstName": "Jane",
      "orderId": "1234"
    }
  }
}'
```

When using `template`, top-level `subject`, `html`, and `text` fields must not be included — pick one approach or the other.

## Response

`status` is one of:

| Value      | Meaning                                                                               |
| ---------- | ------------------------------------------------------------------------------------- |
| `accepted` | Message accepted and queued for delivery.                                             |
| `delayed`  | Transient error during submission. Helo will retry automatically.                     |
| `failed`   | Validation or account error. The response includes an `errorCode` and `errorMessage`. |

A successful response always includes a `messageId` you can use for support lookups.
If some recipients were on your suppression list, they appear in the `suppressions` array — the message still sends to the remaining recipients.

```json theme={null}
{
  "status": "accepted",
  "messageId": "01930f2a-4b3c-7d8e-9f0a-1b2c3d4e5f6a",
  "suppressions": ["previously-bounced@example.com"]
}
```

If all recipients are suppressed, the message is rejected with `errorCode: "recipients_suppressed"` rather than sending to no one.

## Batch sending

```
POST /send/transactional/batch
```

Sends multiple messages in a single request. Each message is validated and processed independently — one failure does not affect the others.

**Example request**

```bash command-line theme={null}
curl --location 'https://api.helohq.com/send/transactional/batch' \
--header "X-Helo-Channel-Id: $HELO_CHANNEL_ID" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "requests": [
    {
      "from": "hello@acme.com",
      "to": [{ "email": "jane@example.com" }],
      "subject": "Your receipt",
      "html": "<p>Thanks, Jane.</p>"
    },
    {
      "from": "hello@acme.com",
      "to": [{ "email": "john@example.com" }],
      "subject": "Your receipt",
      "html": "<p>Thanks, John.</p>"
    }
  ]
}'
```

**Response**

```json theme={null}
{
  "responses": [
    { "status": "accepted", "messageId": "01930f2a-...", "suppressions": [] },
    { "status": "accepted", "messageId": "01930f2b-...", "suppressions": [] }
  ]
}
```

## Idempotency

To prevent duplicate sends when retrying a failed request, include an idempotency key:

```
X-Helo-Idempotency-Key: your-unique-key-here
```

If Helo receives the same key with the same request body within one hour, it returns the original response without sending again. If the same key is reused with a different body, the request is rejected with HTTP 409.

Idempotency only applies to successful responses — if a request fails, you can safely retry it with the same key.

## Suppressions

Helo automatically checks every recipient against your channel's suppression list before sending. Suppressed addresses (from previous bounces, unsubscribes, or manual additions) are silently removed from the To, Cc, and Bcc fields, and their addresses are returned in the `suppressions` array on the response.

If you need to check whether an address is suppressed before submitting, use the Suppressions API.

## Tracking

By default, open and link tracking follow your channel settings. Override them per-message with the `tracking` object:

```json theme={null}
{
  "tracking": {
    "opens": true,
    "links": false
  }
}
```

## Validation errors

If the API returns `"status": "failed"`, the response includes an `errorCode`.

See [API overview](/api-reference/overview#error-codes) for specific error codes.

## Channel selection

If your API credential is scoped to a specific channel, no extra configuration is needed. If you're using an account-level credential, specify the channel in the request header:

```
X-Helo-Channel-Id: <channel-uuid>
```
