EnvlopedDocs

Send Email

API reference for sending emails with Envloped

Send Email

Send transactional emails using the Envloped API.

Endpoint

POST /v1/emails

Request Headers

HeaderRequiredDescription
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json
Idempotency-KeyNoMakes 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

ParameterTypeRequiredDescription
fromstringYesSender email address. Must be from a verified domain. Supports "Name <email>" format for a display name.
tostring | string[]YesRecipient email address(es).
subjectstringYesEmail subject line.
htmlstringNo*HTML content of the email.
textstringNo*Plain text content of the email.
replyTostringNoReply-to email address.
ccstring[]NoCC recipient addresses.
bccstring[]NoBCC recipient addresses.
headersobjectNoCustom email headers.
attachmentsarrayNoFile 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

FieldTypeDescription
successbooleantrue when the email was accepted for sending.
idstringThe email's identifier in Envloped. Pass it to GET /v1/emails/:id to read the send back, including its delivery status.
messageIdstringThe provider's message id, which delivery and bounce events are keyed on. GET /v1/emails/:id accepts this too, so either identifier works.
suppressed_recipientsobject[]Present only when recipients were skipped because they are on your suppression list. Each entry carries email, reason and scope.
suppressedbooleanPresent, 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.

PlanIncluded per billing periodAt the limit
Transactional ($10/mo)20,000 emails, reset each period402 quota_exhausted — buy a pack
Marketing ($29/mo)75,000 campaign sends — 15× your 5,000 included active contacts402 quota_exhausted — buy a pack
Legacy free100 emails, for the lifetime of the account — not per month, and it never resets402 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:

CodeMeaning
free_quota_exhaustedThe 100 lifetime free emails are used. Only a legacy or lapsed account can receive this; subscribe to keep sending
quota_exhaustedIncluded quota and prepaid credits are both at zero. Top up, or turn on auto-recharge
payment_requiredAn unpaid invoice has passed its grace period and sending is blocked. Update the card
account_blockedThe 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:

CodeStatusMeaning
sending_not_approved403This workspace has not been reviewed yet. See Sending approval
reputation_suspended403Bounce or complaint rates crossed the threshold
domain_not_verified403The 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.

On this page