# Send an email

> Queues one email for delivery and returns its id. The from domain must be verified and allowed for the API key. Up to 50 unique recipients across to, cc and bcc; a text part is generated from html…

Source: https://docs.pidgely.com/api-reference/emails/send/

`POST https://api.pidgely.com/emails`

Queues one email for delivery and returns its id. The `from` domain must be [verified](https://docs.pidgely.com/guides/domains/) and allowed for the API key. Up to 50 unique recipients across `to`, `cc` and `bcc`; a text part is generated from `html` when you omit `text`.

`scheduled_at` accepts ISO 8601 or natural language such as `in 1 hour` or `tomorrow at 9am` (UTC), up to 30 days ahead. Attachments take base64 `content` or a remote `path`. Send an [`Idempotency-Key`](https://docs.pidgely.com/idempotency/) to retry safely. See [Sending email](https://docs.pidgely.com/guides/sending/) for every option.

**Authorization:** `Bearer pdg_…`. Any API key (`full_access` or `sending_access`).

## Headers

| Name | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | string | No | Replays the stored response for 24 hours; a different body gets 409. Up to 256 characters. |

## Body parameters

JSON object.

| Name | Type | Required | Description |
|---|---|---|---|
| `from` | string | No | `Name <addr@verified-domain>`; may come from the template. |
| `to` | string \| string[] | Yes | Recipient address (`a@x.com` or `Name <a@x.com>`) or a list of them. Up to 50 unique recipients across `to`, `cc` and `bcc`. |
| `cc` | string \| string[] | No | Carbon-copy recipients. Count toward the 50-recipient limit. |
| `bcc` | string \| string[] | No | Blind carbon-copy recipients. Count toward the 50-recipient limit. |
| `reply_to` | string \| string[] | No | Reply-To address or list of addresses. |
| `subject` | string | No | Subject line, up to 998 bytes. Required unless a template provides it. |
| `html` | string | No | HTML body. `html` or `text` is required (unless you send with a template). |
| `text` | string | No | Generated from html when omitted. |
| `headers` | object (map of string) | No | Custom headers. Platform-managed headers (From, To, Subject, Message-ID, DKIM-Signature, Return-Path, Reply-To, …) are rejected; values cannot contain line breaks. |
| `attachments` | object[] | No | Files to attach. All attachments share a 40 MiB budget (decoded). Executable types (`.exe`, `.js`, `.bat`, …) are blocked. |
| `attachments[].filename` | string | No | File name shown to the recipient. Defaults to the last segment of `path`. |
| `attachments[].content` | string | No | Base64 content. Use either content or path. |
| `attachments[].path` | string | No | http(s) URL downloaded at send time (SSRF-guarded, 40MB budget, no redirects). |
| `attachments[].content_type` | string | No | MIME type. Guessed from the file extension when omitted. |
| `attachments[].content_id` | string | No | Makes the attachment inline (`cid:` in HTML). |
| `tags` | object[] | No | Name/value pairs attached to the email and returned in webhooks. |
| `tags[].name` | string | Yes | Display name. Pattern: `^[A-Za-z0-9_-]{1,256}$`. |
| `tags[].value` | string | Yes | Pattern: `^[A-Za-z0-9_-]{1,256}$`. |
| `scheduled_at` | string | No | ISO 8601 or natural language (`in 1 hour`, `tomorrow at 9am`); within 30 days. |
| `template` | object | No | — |
| `template.id` | string | Yes | Template id or alias (published version is used). |
| `template.variables` | object (map of string \| number) | No | Values for the template variables: strings up to 2,000 characters or numbers. |
| `topic_id` | string | No | Not supported yet; a non-empty value is rejected. |
| `stream` | string | No | Pidgely extension: `transactional` (default) or `broadcast` (adds RFC 8058 unsubscribe). One of `transactional`, `broadcast`. |

## Request

```js tab="Node.js"
import { Pidgely } from 'pidgely';

const pidgely = new Pidgely(process.env.PIDGELY_API_KEY);

const { data, error } = await pidgely.emails.send({
  from: 'Acme <onboarding@acme.com>',
  to: ['delivered@example.com'],
  subject: 'Welcome to Acme',
  html: '<p>Thanks for signing up!</p>',
  tags: [
    {
      name: 'category',
      value: 'welcome',
    },
  ],
}, {
  idempotencyKey: 'welcome-user-123',
});
console.log(data ?? error);
```

```python tab="Python"
import os
import pidgely

pidgely.api_key = os.environ["PIDGELY_API_KEY"]

result = pidgely.Emails.send({
    "from": "Acme <onboarding@acme.com>",
    "to": ["delivered@example.com"],
    "subject": "Welcome to Acme",
    "html": "<p>Thanks for signing up!</p>",
    "tags": [
        {
            "name": "category",
            "value": "welcome",
        },
    ],
}, {
    "idempotency_key": "welcome-user-123",
})
print(result)
```

```php tab="PHP"
<?php

require __DIR__ . '/vendor/autoload.php';

$pidgely = Pidgely::client(getenv('PIDGELY_API_KEY'));

$result = $pidgely->emails->send([
    'from' => 'Acme <onboarding@acme.com>',
    'to' => ['delivered@example.com'],
    'subject' => 'Welcome to Acme',
    'html' => '<p>Thanks for signing up!</p>',
    'tags' => [
        [
            'name' => 'category',
            'value' => 'welcome',
        ],
    ],
], [
    'idempotency_key' => 'welcome-user-123',
]);
var_dump($result);
```

```go tab="Go"
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/pidgely/pidgely-go"
)

func main() {
	ctx := context.Background()
	client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY"))

	result, err := client.Emails.Send(ctx, &pidgely.SendEmailRequest{
		From:    "Acme <onboarding@acme.com>",
		To:      []string{"delivered@example.com"},
		Subject: "Welcome to Acme",
		Html:    "<p>Thanks for signing up!</p>",
		Tags:    []pidgely.Tag{
		{
			Name:  "category",
			Value: "welcome",
		},
	},
	}, pidgely.WithIdempotencyKey("welcome-user-123"))
	if err != nil {
		panic(err)
	}
	fmt.Println(result)
}
```

```ruby tab="Ruby"
require "pidgely"

Pidgely.api_key = ENV["PIDGELY_API_KEY"]

result = Pidgely::Emails.send({
  from: "Acme <onboarding@acme.com>",
  to: ["delivered@example.com"],
  subject: "Welcome to Acme",
  html: "<p>Thanks for signing up!</p>",
  tags: [
    {
      name: "category",
      value: "welcome"
    }
  ]
}, {
  idempotency_key: "welcome-user-123"
})
puts result
```

```bash tab="cURL"
curl -X POST 'https://api.pidgely.com/emails' \
  -H "Authorization: Bearer $PIDGELY_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: welcome-user-123' \
  -d '{
  "from": "Acme <onboarding@acme.com>",
  "to": [
    "delivered@example.com"
  ],
  "subject": "Welcome to Acme",
  "html": "<p>Thanks for signing up!</p>",
  "tags": [
    {
      "name": "category",
      "value": "welcome"
    }
  ]
}'
```

## Response

`200 OK`

```json
{
  "id": "0199d8a2-6f3c-7c51-9e2a-3b1f0c6d4e21"
}
```

### Response fields

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | Yes | Unique id (UUID). UUID. |

## Errors

Errors have the shape `{"statusCode", "name", "message"}`. See [Errors](https://docs.pidgely.com/errors/) for the full list.

| Status | Name | When |
|---|---|---|
| 401 | `missing_api_key`, `restricted_api_key` | No `Authorization: Bearer` header, or a `sending_access` key called an endpoint that needs `full_access`. |
| 403 | `invalid_api_key`, `validation_error` | The API key is invalid or revoked, or the request is not allowed for this account (unverified domain, plan limit, paused account). |
| 404 | `not_found` | The resource does not exist in this account (or the id is not a valid UUID). |
| 422 | `validation_error` | A field is missing or invalid; `message` names it. |
| 429 | `rate_limit_exceeded` | Too many requests per second. Wait for `Retry-After` seconds. |
| 403 | `validation_error` | The `from` domain is not verified, the key is restricted to another domain, or sending is paused for the account. |
| 409 | `invalid_idempotent_request` | The `Idempotency-Key` was already used with a different request body. |
| 400 | `invalid_idempotency_key` | The `Idempotency-Key` is longer than 256 characters. |
| 422 | `invalid_attachment` | An attachment has neither `content` nor `path`. |
| 429 | `daily_quota_exceeded` | The daily sending quota is used up (UTC day). |
| 429 | `monthly_quota_exceeded` | The monthly sending quota is used up. |
