# Send an email

> POST /emails — send a single transactional email.

`POST /emails`

Send a single email from a verified domain. The `from` address must belong to a domain you have verified; provide at least one of `html` or `text`. Returns the created email's `id`.

Pass an optional `Idempotency-Key` header to make retries safe — see [Idempotency keys](https://www.sendping.co/docs/emails/idempotency).

## Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer API key, e.g. `Bearer mb_xxxxxxxxx`. |
| `Idempotency-Key` | string | No | Optional unique key so a retried request is processed only once (remembered 24h). |

## Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string | Yes | Sender address on a verified domain. Accepts `Name <addr@domain.com>` or a bare address. |
| `to` | string | string[] | Yes | Recipient address(es). Between 1 and 50 recipients. |
| `subject` | string | Yes | The email subject line. |
| `html` | string | No | HTML body. Provide `html` or `text` (or both). Markdown-style `[text](url)` links and bare URLs (`https://…` / `www.…`) in the body text are converted to tracked hyperlinks automatically at send time; content already inside `<a>` tags, attribute values, and `<pre>`/`<code>` blocks is left untouched. |
| `text` | string | No | Plain-text body. Provide `text` or `html` (or both). |
| `preview_text` | string | No | Inbox preview text (preheader) shown next to the subject in the recipient's inbox list. Injected as a hidden element at the top of the HTML body — never visible in the opened email, and excluded from the plain-text part. Max **150 characters**. |
| `cc` | string | string[] | No | Carbon-copy recipient(s). Up to 50. |
| `bcc` | string | string[] | No | Blind carbon-copy recipient(s). Up to 50. |
| `reply_to` | string | string[] | No | Reply-To address(es). |
| `headers` | object | No | Map of custom MIME header name → value. See [Custom headers](https://www.sendping.co/docs/emails/headers). |
| `attachments` | object[] | No | Files to attach (max **25 MB per file** and **40 MB total per email**, measured on the decoded bytes — base64 inflates the request body by roughly 33%). Each item is `{ filename, content | path, content_type, content_id }`. See [Attachments](https://www.sendping.co/docs/emails/attachments). |
| `scheduled_at` | string | No | Schedule the email to be sent later. Natural language (e.g. `in 1 min`) or ISO 8601 (e.g. `2026-08-05T11:52:01.858Z`). See [Schedule email](https://www.sendping.co/docs/emails/schedule). |
| `topic_id` | string | No | Topic ID for subscription handling. Each `to`/`cc`/`bcc` address is checked against the topic: recipients who are unsubscribed from the topic are **skipped** (removed from the recipient lists) and the message is sent to the rest. If every `to` recipient is skipped, the request is rejected with a `validation_error`. Addresses that are not contacts are kept unless they have unsubscribed from the topic themselves. An unknown `topic_id` returns a `validation_error`. Setting `topic_id` also marks the send as subscription mail, so SendPing adds a per-recipient unsubscribe footer and the RFC 8058 `List-Unsubscribe` headers; opting out removes that address from **this topic only** — it is not a suppression and does not affect your other email. A send **without** `topic_id` carries no unsubscribe link or header. See [Unsubscribe links](https://www.sendping.co/docs/emails/unsubscribe). |
| `template` | object | No | Send using a published template instead of inline body: `{ id, variables }`. `id` is the template id or alias; `variables` is an object of key/value pairs. When a `template` is set you cannot also send `html`/`text`; `from`, `subject`, and `reply_to` in the payload take precedence over the template defaults. |

### attachments[] properties

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string | No | Name of the attached file. |
| `content` | string | No | Content of the attached file, passed as a Base64 string. Provide `content` or `path`. |
| `path` | string | No | URL where the attachment file is hosted; fetched at send time. Provide `path` or `content`. |
| `content_type` | string | No | Content type for the attachment. Derived from `filename` when not set. |
| `content_id` | string | No | Embeds the file as an inline image: reference it in your HTML via `<img src="cid:...">`. |

> **Note:** The `Idempotency-Key` header should be unique per API request, has a maximum length of **255 characters**, and expires after **24 hours**.

## Request

**Node.js**

```js
import { SendPing } from 'sendping';

const mb = new SendPing('mb_xxxxxxxxx');

const { data, error } = await mb.emails.send({
  "from": "Acme <hello@yourdomain.com>",
  "to": ["delivered@test.sendping.co"],
  "subject": "Hello from SendPing",
  "html": "<p>It works!</p>"
});
console.log({ data, error });
```

**Ruby**

```ruby
require "sendping"

SendPing.api_key = "mb_xxxxxxxxx"

SendPing::Emails.send({
  "from": "Acme <hello@yourdomain.com>",
  "to": [
    "delivered@test.sendping.co"
  ],
  "subject": "Hello from SendPing",
  "html": "<p>It works!</p>"
})
```

**PHP**

```php
<?php
require 'vendor/autoload.php';

use SendPing\SendPing;

$sendping = SendPing::client('mb_xxxxxxxxx');

$sendping->emails->send([
  'from' => "Acme <hello@yourdomain.com>",
  'to' => [
    "delivered@test.sendping.co"
  ],
  'subject' => "Hello from SendPing",
  'html' => "<p>It works!</p>"
]);
```

**Python**

```python
import sendping

sendping.api_key = "mb_xxxxxxxxx"

sendping.Emails.send({
  "from": "Acme <hello@yourdomain.com>",
  "to": [
    "delivered@test.sendping.co"
  ],
  "subject": "Hello from SendPing",
  "html": "<p>It works!</p>"
})
```

**Go**

```go
import "github.com/shekhu10/sendping-sdks/sendping-go"

client := sendping.NewClient("mb_xxxxxxxxx")

sent, err := client.Emails.Send(&sendping.SendEmailRequest{
    From:    "Acme <hello@yourdomain.com>",
    To:      []string{"delivered@test.sendping.co"},
    Subject: "Hello from SendPing",
    Html:    "<p>It works!</p>",
})
```

**Rust**

```rust
use sendping::{SendEmailOptions, SendPing};

let mb = SendPing::new("mb_xxxxxxxxx");

let params = SendEmailOptions::new(
    "Acme <hello@yourdomain.com>",
    ["delivered@test.sendping.co"],
    "Hello from SendPing",
)
.with_html("<p>It works!</p>");
let _sent = mb.emails.send(params).await?;
```

**Java**

```java
import co.sendping.SendPing;
import co.sendping.SendPingResponse;
import co.sendping.requests.SendEmailRequest;

SendPing sendping = new SendPing("mb_xxxxxxxxx");

SendEmailRequest request = SendEmailRequest.builder()
        .from("Acme <hello@yourdomain.com>")
        .to("delivered@test.sendping.co")
        .subject("Hello from SendPing")
        .html("<p>It works!</p>")
        .build();

SendPingResponse response = sendping.emails().send(request);
```

**.NET**

```csharp
using SendPing;

ISendPing sendping = SendPingClient.Create("mb_xxxxxxxxx");

var resp = await sendping.EmailSendAsync(new EmailMessage
{
    From = "Acme <hello@yourdomain.com>",
    To = "delivered@test.sendping.co",
    Subject = "Hello from SendPing",
    HtmlBody = "<p>It works!</p>",
});
```

**cURL**

```bash
curl -X POST 'https://www.sendping.co/api/emails' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
  "from": "Acme <hello@yourdomain.com>",
  "to": ["delivered@test.sendping.co"],
  "subject": "Hello from SendPing",
  "html": "<p>It works!</p>"
}'
```

**CLI**

```bash
sendping emails send \
  --from 'Acme <hello@yourdomain.com>' \
  --to 'delivered@test.sendping.co' \
  --subject 'Hello from SendPing' \
  --html '<p>It works!</p>'
```

## Response

```json
{
  "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
}
```

## Errors

Returns `missing_required_field` if `from`, `to`, or `subject` is missing; `validation_error` if neither `html` nor `text` is provided, if `to` is outside 1–50 recipients, or if the `from` domain is not verified; `invalid_from_address` / `invalid_to_address` for malformed addresses; and `409 concurrent_idempotent_requests` if a request with the same in-flight `Idempotency-Key` is already processing. See the [error reference](https://www.sendping.co/docs/api/errors).
