# Send email

`POST /v1/emails` takes a JSON body and returns `201` with the new email's `id`. The email is queued immediately and sent within seconds; its status and events are available at `GET /v1/emails/:id`.

## A full request

**curl**

```bash
curl -X POST https://api-staging.avelto.dev/v1/emails \
  -H "Authorization: Bearer av_live_..." \
  -H "Idempotency-Key: receipt-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <billing@mail.acme.com>",
    "to": [
      "jane@example.com",
      "Finance <finance@example.com>"
    ],
    "cc": "ops@example.com",
    "reply_to": "support@acme.com",
    "subject": "Receipt #1042",
    "html": "<p>Thanks for your order.</p>",
    "text": "Thanks for your order.",
    "headers": {
      "X-Entity-Ref-ID": "1042"
    },
    "tags": [
      "receipt",
      "order:1042"
    ],
    "attachments": [
      {
        "filename": "receipt-1042.pdf",
        "content": "JVBERi0xLjQKJ...",
        "content_type": "application/pdf"
      }
    ]
  }'
```

**Node**

```ts
import { Avelto } from "@avelto/sdk";

const avelto = new Avelto(process.env.AVELTO_API_KEY!);

const { id } = await avelto.emails.send(
  {
    from: "Acme <billing@mail.acme.com>",
    to: ["jane@example.com", "Finance <finance@example.com>"],
    cc: "ops@example.com",
    reply_to: "support@acme.com",
    subject: "Receipt #1042",
    html: "<p>Thanks for your order.</p>",
    text: "Thanks for your order.",
    headers: { "X-Entity-Ref-ID": "1042" },
    tags: ["receipt", "order:1042"],
    attachments: [
      { filename: "receipt-1042.pdf", content: "JVBERi0xLjQKJ...", content_type: "application/pdf" },
    ],
  },
  { idempotencyKey: "receipt-1042" },
);
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api-staging.avelto.dev/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}", "Idempotency-Key": "receipt-1042"},
    json={
      "from": "Acme <billing@mail.acme.com>",
      "to": [
        "jane@example.com",
        "Finance <finance@example.com>"
      ],
      "cc": "ops@example.com",
      "reply_to": "support@acme.com",
      "subject": "Receipt #1042",
      "html": "<p>Thanks for your order.</p>",
      "text": "Thanks for your order.",
      "headers": {
        "X-Entity-Ref-ID": "1042"
      },
      "tags": [
        "receipt",
        "order:1042"
      ],
      "attachments": [
        {
          "filename": "receipt-1042.pdf",
          "content": "JVBERi0xLjQKJ...",
          "content_type": "application/pdf"
        }
      ]
    },
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"from":"Acme <billing@mail.acme.com>","to":["jane@example.com","Finance <finance@example.com>"],"cc":"ops@example.com","reply_to":"support@acme.com","subject":"Receipt #1042","html":"<p>Thanks for your order.</p>","text":"Thanks for your order.","headers":{"X-Entity-Ref-ID":"1042"},"tags":["receipt","order:1042"],"attachments":[{"filename":"receipt-1042.pdf","content":"JVBERi0xLjQKJ...","content_type":"application/pdf"}]}`)
	req, _ := http.NewRequest("POST", "https://api-staging.avelto.dev/v1/emails", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))
	req.Header.Set("Idempotency-Key", "receipt-1042")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api-staging.avelto.dev/v1/emails")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"
req["Idempotency-Key"] = "receipt-1042"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
  from: "Acme <billing@mail.acme.com>",
  to: ["jane@example.com", "Finance <finance@example.com>"],
  cc: "ops@example.com",
  reply_to: "support@acme.com",
  subject: "Receipt #1042",
  html: "<p>Thanks for your order.</p>",
  text: "Thanks for your order.",
  headers: {
    "X-Entity-Ref-ID" => "1042"
  },
  tags: ["receipt", "order:1042"],
  attachments: [{
    filename: "receipt-1042.pdf",
    content: "JVBERi0xLjQKJ...",
    content_type: "application/pdf"
  }]
})

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api-staging.avelto.dev"]);

$res = $client->request("POST", "/v1/emails", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
        "Idempotency-Key" => "receipt-1042",
    ],
    "json" => [
        "from" => "Acme <billing@mail.acme.com>",
        "to" => ["jane@example.com", "Finance <finance@example.com>"],
        "cc" => "ops@example.com",
        "reply_to" => "support@acme.com",
        "subject" => "Receipt #1042",
        "html" => "<p>Thanks for your order.</p>",
        "text" => "Thanks for your order.",
        "headers" => [
            "X-Entity-Ref-ID" => "1042"
        ],
        "tags" => ["receipt", "order:1042"],
        "attachments" => [[
            "filename" => "receipt-1042.pdf",
            "content" => "JVBERi0xLjQKJ...",
            "content_type" => "application/pdf"
        ]]
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));
client.DefaultRequestHeaders.Add("Idempotency-Key", "receipt-1042");

var res = await client.PostAsJsonAsync("https://api-staging.avelto.dev/v1/emails", new
{
    from = "Acme <billing@mail.acme.com>",
    to = new[] { "jane@example.com", "Finance <finance@example.com>" },
    cc = "ops@example.com",
    reply_to = "support@acme.com",
    subject = "Receipt #1042",
    html = "<p>Thanks for your order.</p>",
    text = "Thanks for your order.",
    headers = new Dictionary<string, object> { ["X-Entity-Ref-ID"] = "1042" },
    tags = new[] { "receipt", "order:1042" },
    attachments = new[] { new { filename = "receipt-1042.pdf", content = "JVBERi0xLjQKJ...", content_type = "application/pdf" } }
});
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

## Fields

| Field | Type | Notes |
| --- | --- | --- |
| `from` | string | Required. `a@b.com` or `Name <a@b.com>`. The domain must be [verified](/docs/domains) on your account, or be your sandbox domain. |
| `to` | string or string[] | Required. Up to 50 recipients across `to`, `cc` and `bcc`. |
| `cc`, `bcc` | string or string[] | Optional. |
| `reply_to` | string | Optional. |
| `subject` | string | Required unless you name a template. Up to 998 characters. Line breaks are rejected. |
| `html`, `text` | string | At least one is required, unless you name a template. Up to 5 MB each. |
| `template_id`, `template_slug` | string | Optional. Send a stored [template](/docs/templates) instead of a body. Name it one way or the other, not both. The template supplies the subject and body, so `subject`, `html` and `text` are rejected alongside it. |
| `variables` | object | Optional. Values for the template's `{{variables}}`. Strings, numbers, booleans or `null`. Names must match `^[A-Za-z_][A-Za-z0-9_]*$`; string values are capped at 10,000 characters. Rejected without a template. |
| `headers` | object | Optional. Extra headers as `name: value`. Names up to 78 characters, values up to 1000, no control characters. Names that Avelto sets itself (`From`, `To`, `Subject`, `Reply-To`, `Sender`, `Return-Path`, `Date`, `Message-ID`, `Received`, `DKIM-Signature`, ARC and `Authentication-Results` headers, MIME headers and anything starting `X-SES`) are rejected. Use `reply_to` rather than a `Reply-To` header. |
| `tags` | string[] | Optional. Up to 10. Letters, numbers, `_ - : .`, up to 64 characters each. Filter lists by tag. |
| `attachments` | object[] | Optional. Up to 10 files, 7 MB in total. Each has a `filename`, an optional `content_type`, and either base64 `content` or a `url` we fetch. |
| `scheduled_at` | string | Optional. ISO 8601 timestamp in the future, at most 30 days ahead. |
| `unsubscribe_url` | string | Optional. An https URL that unsubscribes the recipient. Setting it marks the message as bulk and adds the `List-Unsubscribe` and `List-Unsubscribe-Post` (one-click) headers. The URL must accept an unauthenticated POST. Do not also set a `List-Unsubscribe` header, and leave it off transactional mail. |

Addresses with line breaks, control characters in any header field, and filenames containing quotes, slashes or backslashes are all rejected with `validation_error`.

## Attachments

Give each attachment the bytes, or somewhere to get them:

```json
{
  "attachments": [
    { "filename": "invoice.pdf", "content": "JVBERi0xLjQK..." },
    { "filename": "logo.png", "url": "https://cdn.acme.com/logo.png" }
  ]
}
```

A `url` is fetched when the send is accepted, not later, so a broken link is an error on
the call that made it rather than a surprise two seconds afterwards. It must be reachable
from the public internet — private and loopback addresses are refused, and redirects are
not followed, so give us the final URL. It must be https, answer within ten seconds, and
return a non-empty body.

Ten files, 7 MB in total after decoding. The limit is checked again after any URLs are
fetched, because their size is not knowable before that. Filenames may not contain quotes,
slashes or backslashes.

Attachment names, types and sizes appear on the email in your dashboard. The bytes
themselves are not available for download.

## Sending a batch

Up to 100 messages in one call:

**curl**

```bash
curl -X POST https://api-staging.avelto.dev/v1/emails/batch \
  -H "Authorization: Bearer av_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "from": "Acme <billing@mail.acme.com>",
        "to": "jane@example.com",
        "subject": "Receipt #1042",
        "text": "Thanks for your order."
      },
      {
        "from": "Acme <billing@mail.acme.com>",
        "to": "sam@example.com",
        "subject": "Receipt #1043",
        "text": "Thanks for your order."
      }
    ]
  }'
```

**Node**

```ts
const batch = await avelto.emails.sendBatch({
  messages: [
    { from: "Acme <billing@mail.acme.com>", to: "jane@example.com", subject: "Receipt #1042", text: "Thanks for your order." },
    { from: "Acme <billing@mail.acme.com>", to: "sam@example.com", subject: "Receipt #1043", text: "Thanks for your order." },
  ],
});
for (const r of batch.results) console.log(r.index, r.ok ? r.id : r.error.code);
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api-staging.avelto.dev/v1/emails/batch",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}"},
    json={
      "messages": [
        {
          "from": "Acme <billing@mail.acme.com>",
          "to": "jane@example.com",
          "subject": "Receipt #1042",
          "text": "Thanks for your order."
        },
        {
          "from": "Acme <billing@mail.acme.com>",
          "to": "sam@example.com",
          "subject": "Receipt #1043",
          "text": "Thanks for your order."
        }
      ]
    },
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"messages":[{"from":"Acme <billing@mail.acme.com>","to":"jane@example.com","subject":"Receipt #1042","text":"Thanks for your order."},{"from":"Acme <billing@mail.acme.com>","to":"sam@example.com","subject":"Receipt #1043","text":"Thanks for your order."}]}`)
	req, _ := http.NewRequest("POST", "https://api-staging.avelto.dev/v1/emails/batch", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api-staging.avelto.dev/v1/emails/batch")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
  messages: [{
    from: "Acme <billing@mail.acme.com>",
    to: "jane@example.com",
    subject: "Receipt #1042",
    text: "Thanks for your order."
  }, {
    from: "Acme <billing@mail.acme.com>",
    to: "sam@example.com",
    subject: "Receipt #1043",
    text: "Thanks for your order."
  }]
})

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api-staging.avelto.dev"]);

$res = $client->request("POST", "/v1/emails/batch", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
    ],
    "json" => [
        "messages" => [[
            "from" => "Acme <billing@mail.acme.com>",
            "to" => "jane@example.com",
            "subject" => "Receipt #1042",
            "text" => "Thanks for your order."
        ], [
            "from" => "Acme <billing@mail.acme.com>",
            "to" => "sam@example.com",
            "subject" => "Receipt #1043",
            "text" => "Thanks for your order."
        ]]
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));

var res = await client.PostAsJsonAsync("https://api-staging.avelto.dev/v1/emails/batch", new
{
    messages = new[] { new { from = "Acme <billing@mail.acme.com>", to = "jane@example.com", subject = "Receipt #1042", text = "Thanks for your order." }, new { from = "Acme <billing@mail.acme.com>", to = "sam@example.com", subject = "Receipt #1043", text = "Thanks for your order." } }
});
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

Each message is validated, limited and queued exactly as a single send is — the batch is
a convenience for the round trip, not a transaction. One suppressed address in a run of a
hundred costs you that message and nothing else, so the response reports per message:

```json
{
  "results": [
    { "ok": true, "index": 0, "id": "5f1c..." },
    { "ok": false, "index": 1, "error": { "code": "recipient_suppressed", "message": "..." } }
  ],
  "accepted": 1,
  "failed": 1
}
```

`results[i]` lines up with `messages[i]`. The status is `200` when everything was
accepted, `422` when nothing was, and `207` when it was mixed — so an all-or-nothing
outcome looks like the single-send equivalent and your existing error handling still
works.

Send an `Idempotency-Key` and the whole batch becomes replayable: a retry after a timeout
returns the same ids instead of sending again. Each message counts separately against
your plan; the batch is a single request against the per-second rate limit.

## Idempotency

Retries are safe when you send an `Idempotency-Key` header. The first request creates the email; a replay with the same key on the same account returns the same `id` with status `200` and does not send again. Keys are kept per account; the first 255 characters are used (200 for a batch). A replay is matched on the key alone, not the body. Send one with every request you might repeat, and retry on `429`, `502`, `503`, `504` and network errors with a short backoff. The [Node SDK](/docs/sdk) does both automatically: a random key per call unless you pass one, and up to three attempts.

```ts
const { id } = await avelto.emails.send(
  {
    from: "billing@mail.acme.com",
    to: "jane@example.com",
    subject: "Receipt #1042",
    text: "...",
  },
  { idempotencyKey: "receipt-1042" },
);
```

## Schedule and cancel

Set `scheduled_at` to hold the email until then. It sits in status `scheduled` and can be cancelled until it is sent.

**curl**

```bash
curl -X POST https://api-staging.avelto.dev/v1/emails \
  -H "Authorization: Bearer av_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mail.acme.com>",
    "to": "jane@example.com",
    "subject": "Your trial ends tomorrow",
    "text": "Reply to this email if you have questions.",
    "scheduled_at": "2026-10-01T09:00:00Z"
  }'
```

**Node**

```ts
const { id } = await avelto.emails.send({
  from: "Acme <hello@mail.acme.com>",
  to: "jane@example.com",
  subject: "Your trial ends tomorrow",
  text: "Reply to this email if you have questions.",
  scheduled_at: "2026-10-01T09:00:00Z",
});
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api-staging.avelto.dev/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}"},
    json={
      "from": "Acme <hello@mail.acme.com>",
      "to": "jane@example.com",
      "subject": "Your trial ends tomorrow",
      "text": "Reply to this email if you have questions.",
      "scheduled_at": "2026-10-01T09:00:00Z"
    },
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"from":"Acme <hello@mail.acme.com>","to":"jane@example.com","subject":"Your trial ends tomorrow","text":"Reply to this email if you have questions.","scheduled_at":"2026-10-01T09:00:00Z"}`)
	req, _ := http.NewRequest("POST", "https://api-staging.avelto.dev/v1/emails", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api-staging.avelto.dev/v1/emails")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
  from: "Acme <hello@mail.acme.com>",
  to: "jane@example.com",
  subject: "Your trial ends tomorrow",
  text: "Reply to this email if you have questions.",
  scheduled_at: "2026-10-01T09:00:00Z"
})

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api-staging.avelto.dev"]);

$res = $client->request("POST", "/v1/emails", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
    ],
    "json" => [
        "from" => "Acme <hello@mail.acme.com>",
        "to" => "jane@example.com",
        "subject" => "Your trial ends tomorrow",
        "text" => "Reply to this email if you have questions.",
        "scheduled_at" => "2026-10-01T09:00:00Z"
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));

var res = await client.PostAsJsonAsync("https://api-staging.avelto.dev/v1/emails", new
{
    from = "Acme <hello@mail.acme.com>",
    to = "jane@example.com",
    subject = "Your trial ends tomorrow",
    text = "Reply to this email if you have questions.",
    scheduled_at = "2026-10-01T09:00:00Z"
});
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

**curl**

```bash
curl -X POST https://api-staging.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel \
  -H "Authorization: Bearer av_live_..."
```

**Node**

```ts
const { status } = await avelto.emails.cancel("9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10");
console.log(status); // "cancelled"
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api-staging.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}"},
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("POST", "https://api-staging.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api-staging.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api-staging.avelto.dev"]);

$res = $client->request("POST", "/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));

var res = await client.PostAsync("https://api-staging.avelto.dev/v1/emails/9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10/cancel");
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

```json
{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10", "status": "cancelled" }
```

Cancelling an email that is not `scheduled` returns `409 not_scheduled`.

## List emails

`GET /v1/emails` returns the newest first, 20 per page by default and up to 100. Filter with `status`, `tag` and `mode`; `mode` defaults to the mode of the key you call with. `q` (up to 200 characters) searches for a substring of a recipient, the subject or the message id, or an exact email id; each row then carries `match: { field, value }` saying what it was found in.

**curl**

```bash
curl "https://api-staging.avelto.dev/v1/emails?status=bounced&tag=receipt&limit=50" \
  -H "Authorization: Bearer av_live_..."
```

**Node**

```ts
const page = await avelto.emails.list({ status: "bounced", tag: "receipt", limit: 50 });
console.log(page.data.length, page.next_cursor);
```

**Python**

```python
import os, requests

r = requests.get(
    "https://api-staging.avelto.dev/v1/emails?status=bounced&tag=receipt&limit=50",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}"},
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://api-staging.avelto.dev/v1/emails?status=bounced&tag=receipt&limit=50", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api-staging.avelto.dev/v1/emails?status=bounced&tag=receipt&limit=50")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api-staging.avelto.dev"]);

$res = $client->request("GET", "/v1/emails?status=bounced&tag=receipt&limit=50", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));

var res = await client.GetAsync("https://api-staging.avelto.dev/v1/emails?status=bounced&tag=receipt&limit=50");
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

```json
{
  "data": [
    {
      "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10",
      "mode": "live",
      "from": "Acme <billing@mail.acme.com>",
      "to": ["jane@example.com"],
      "cc": [],
      "bcc": [],
      "reply_to": null,
      "subject": "Receipt #1042",
      "tags": ["receipt", "order:1042"],
      "status": "bounced",
      "scheduled_at": null,
      "created_at": "2026-09-17T10:12:04.000Z",
      "updated_at": "2026-09-17T10:12:09.000Z"
    }
  ],
  "next_cursor": null
}
```

Pass `next_cursor` back as `cursor` to get the next page. It is `null` on the last page.

## Statuses

| Status | Meaning |
| --- | --- |
| `queued` | Accepted and waiting to be handed to the mail provider. |
| `scheduled` | Held until `scheduled_at`. |
| `sent` | Handed off. Waiting for a delivery result. |
| `delivered` | Accepted by the recipient's mail server. |
| `bounced` | Rejected by the recipient's mail server. Hard bounces add the address to your [suppressions](/docs/suppressions). |
| `complained` | The recipient marked it as spam. The address is suppressed. |
| `failed` | Could not be sent after retries. `error` says why. |
| `cancelled` | A scheduled email you cancelled. |

## Errors

Every error is a JSON envelope with a stable `code` and a human-readable `message`. Validation errors add `details`.

```http
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "code": "domain_not_verified",
    "message": "Domain mail.acme.com is pending. Add the DNS records and wait for verification before sending."
  }
}
```

```json
{
  "error": {
    "code": "validation_error",
    "message": "html: Provide html or text (or both)",
    "details": [
      { "path": "html", "message": "Provide html or text (or both)" }
    ]
  }
}
```

Codes you will meet when sending:

| Code | Status | When |
| --- | --- | --- |
| `validation_error` | 400 (422 when an attachment `url` could not be fetched, or the fetched files exceed the size limit) | The body failed validation. `details` lists the failing fields. |
| `unauthorized` | 401 | Missing or invalid API key. |
| `forbidden` | 403 | The key does not have the scope for this call (the message names it), or a test key was used where a live one is needed. |
| `domain_not_verified` | 403 | `from` uses a domain that is not added and verified on your account. |
| `sandbox_recipient_not_allowed` | 403 | Sending from the sandbox domain to anyone other than your account email. |
| `account_paused` | 403 | Sending is paused, usually after a high bounce or complaint rate. |
| `account_suspended` | 403 | The account has been suspended by us; the message says why and the dashboard has a button to write to us. |
| `recipient_suppressed` | 422 | A recipient is on your suppression list. `details.suppressed` lists them. |
| `plan_limit` | 429 | Monthly limit reached on Free, or the first-week daily cap of 100 emails. |
| `rate_limited` | 429 | More requests a second than your plan allows on one key (10 on Free and Pro, 25 on Growth, 50 on Enterprise). Honour `Retry-After`. |

The full list is in the [API reference](/docs/api#errors).

---

Rendered page: https://staging.avelto.dev/docs/send-email
