Send Email
API reference for sending emails with Envloped
Send Email
Send transactional emails using the Envloped API.
Endpoint
POST /v1/emailsRequest Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
Idempotency-Key | No | Makes the send safe to retry — a replay returns the original response instead of sending a second email. Strongly recommended for anything triggered by user action. See Idempotency. |
Request Body
{
"from": "Your Name <sender@yourdomain.com>",
"to": "recipient@example.com",
"subject": "Your subject line",
"html": "<p>Your HTML content</p>",
"text": "Your plain text content",
"replyTo": "replies@yourdomain.com",
"cc": ["cc@example.com"],
"bcc": ["bcc@example.com"],
"headers": {
"X-Custom-Header": "value"
},
"attachments": [
{
"filename": "invite.ics",
"content": "QkVHSU46VkNBTEVOREFS...",
"contentType": "text/calendar"
}
]
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Sender email address. Must be from a verified domain. Supports "Name <email>" format for a display name. |
to | string | string[] | Yes | Recipient email address(es). |
subject | string | Yes | Email subject line. |
html | string | No* | HTML content of the email. |
text | string | No* | Plain text content of the email. |
replyTo | string | No | Reply-to email address. |
cc | string[] | No | CC recipient addresses. |
bcc | string[] | No | BCC recipient addresses. |
headers | object | No | Custom email headers. |
attachments | array | No | File attachments (max 10, total ≤ 40MB). See Attachments. |
*At least one of html or text is required.
Response
Success (200 OK)
{
"success": true,
"id": "ac5130c0-060b-40e1-ac7d-ab4204628dbc",
"messageId": "010e01a0c45bc5c8-fad38ef0-7f65-4f1e-bfd4-43b891835314-000000"
}Response Fields
| Field | Type | Description |
|---|---|---|
success | boolean | true when the email was accepted for sending. |
id | string | The email's identifier in Envloped. Pass it to GET /v1/emails/:id to read the send back, including its delivery status. |
messageId | string | The provider's message id, which delivery and bounce events are keyed on. GET /v1/emails/:id accepts this too, so either identifier works. |
suppressed_recipients | object[] | Present only when recipients were skipped because they are on your suppression list. Each entry carries email, reason and scope. |
suppressed | boolean | Present, and true, only when every recipient was suppressed — nothing was sent, and the record's status is suppressed. |
To read the send back — its status, timestamps, body and attachments — call:
GET /v1/emails/{id}The status on that record starts at sent and advances as the provider reports
back: delivered, bounced, complained, or failed. Two less obvious values:
suppressed means every recipient was on your suppression list and nothing was
sent, and unknown means the provider accepted the message but returned no
message id — it was almost certainly delivered, but no delivery events will ever
arrive for it, so it is not reported as sent.
Error Responses
400 Bad Request
{
"error": {
"code": "validation_error",
"message": "Invalid request body",
"details": [
{
"field": "to",
"message": "Invalid email address format"
}
]
}
}401 Unauthorized
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key"
}
}403 Forbidden
{
"error": {
"code": "domain_not_verified",
"message": "The sender domain is not verified"
}
}429 Too Many Requests
Returned when a daily or monthly volume limit is reached — see Limits. Despite the code name there is no per-minute request rate limit today, so retrying sooner will not help; the limit resets on the day or billing period boundary.
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later.",
"retryAfter": 60
}
}Code Examples
cURL
curl -X POST https://api.envloped.com/v1/emails \
-H "Authorization: Bearer en_abc123..." \
-H "Content-Type: application/json" \
-d '{
"from": "Your App <hello@yourdomain.com>",
"to": "user@example.com",
"subject": "Welcome to our service",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"text": "Welcome! Thanks for signing up."
}'JavaScript / TypeScript
async function sendEmail() {
const response = await fetch('https://api.envloped.com/v1/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ENVLOPED_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'Your App <hello@yourdomain.com>',
to: 'user@example.com',
subject: 'Welcome to our service',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
text: 'Welcome! Thanks for signing up.',
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error.message);
}
return response.json();
}Python
import os
import requests
def send_email():
response = requests.post(
'https://api.envloped.com/v1/emails',
headers={
'Authorization': f'Bearer {os.environ["ENVLOPED_API_KEY"]}',
'Content-Type': 'application/json',
},
json={
'from': 'Your App <hello@yourdomain.com>',
'to': 'user@example.com',
'subject': 'Welcome to our service',
'html': '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
'text': 'Welcome! Thanks for signing up.',
}
)
response.raise_for_status()
return response.json()Go
package main
import (
"bytes"
"encoding/json"
"net/http"
"os"
)
type EmailRequest struct {
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
HTML string `json:"html"`
Text string `json:"text"`
}
func sendEmail() (*http.Response, error) {
email := EmailRequest{
From: "Your App <hello@yourdomain.com>",
To: "user@example.com",
Subject: "Welcome to our service",
HTML: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
Text: "Welcome! Thanks for signing up.",
}
body, _ := json.Marshal(email)
req, _ := http.NewRequest("POST", "https://api.envloped.com/v1/emails", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("ENVLOPED_API_KEY"))
req.Header.Set("Content-Type", "application/json")
return http.DefaultClient.Do(req)
}Limits
Limits are on recipients, not on request rate — and not on requests. One
call with 50 recipients in to costs 50, exactly as 50 calls with one recipient
each would. Transactional sends, campaign sends and campaign test sends all draw
on the same balance.
| Plan | Included per billing period | At the limit |
|---|---|---|
| Transactional ($10/mo) | 20,000 emails, reset each period | 402 quota_exhausted — buy a pack |
| Marketing ($29/mo) | 75,000 campaign sends — 15× your 5,000 included active contacts | 402 quota_exhausted — buy a pack |
| Legacy free | 100 emails, for the lifetime of the account — not per month, and it never resets | 402 free_quota_exhausted |
The last row is not a plan you can sign up for. Creating an account takes a payment method and starts a subscription, so the lifetime allowance only describes legacy and lapsed accounts: those made before signup took a card, and subscriptions that were cancelled or went unpaid.
Volume beyond the included quota is prepaid: packs are bought before those emails can be sent, starting at 10,000 emails for $10 and falling to $0.70 and $0.40 per 1,000 at higher monthly volume. Unused pack credits roll over for as long as the subscription is active. Nothing is ever invoiced in arrears, so a send is refused rather than silently adding to a bill.
A refusal is a 402 with a machine-readable code:
| Code | Meaning |
|---|---|
free_quota_exhausted | The 100 lifetime free emails are used. Only a legacy or lapsed account can receive this; subscribe to keep sending |
quota_exhausted | Included quota and prepaid credits are both at zero. Top up, or turn on auto-recharge |
payment_required | An unpaid invoice has passed its grace period and sending is blocked. Update the card |
account_blocked | The account is suspended or closed |
The body carries the remaining balance and a link to top up, so a client can tell "out of credit" from "not allowed to send" without parsing prose.
A send can also be refused for a reason that has nothing to do with money:
| Code | Status | Meaning |
|---|---|---|
sending_not_approved | 403 | This workspace has not been reviewed yet. See Sending approval |
reputation_suspended | 403 | Bounce or complaint rates crossed the threshold |
domain_not_verified | 403 | The from domain has no verified DNS |
sending_not_approved is the one every new account meets first: nothing sends
until a human at Envloped has reviewed the workspace, which takes about a day.
Like the 402, it must never be retried — it clears when a person makes a
decision, not when time passes.
While a subscription is past_due, sending continues for a short grace period
rather than stopping outright — on legacy metered plans that grace is 200
emails per day. Once the grace period ends, sends are refused with 402 payment_required until the payment succeeds.
There is currently no per-minute request rate limit. On legacy metered plans
a volume limit still returns 429; on current plans the balance is the limit and
the refusal is a 402.
Never retry a 402. A 429 clears by waiting; this one clears only when
money moves, so a client that retries it will retry forever. See
Pricing and quotas for how to branch on each code, and for
auto-recharge, which prevents most of these refusals in the first place.
That said, do not treat the absence of a rate limit as a guarantee. Batch where
an endpoint offers batching (see Contacts, which takes 1000
contacts per request), and back off on 429 rather than retrying immediately.