Send a single transactional email through your workspace's verified sending domain. The send is logged and the email's ID is returned. To send many emails in one request, use Batch send; to read back a send's delivery status, use the Emails endpoint.
| Header | Value |
|---|---|
Authorization | Bearer ps_live_YOUR_API_KEY — required |
Content-Type | application/json — required |
JSON object with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
to | string | required | Recipient email address. |
subject | string | required | Email subject line. |
html | string | required | HTML body of the email. |
sender_id | string | optional | ID of a sender configured in your workspace (Settings → Senders). Takes precedence over from. Must belong to your workspace and be on a verified domain, or the request is rejected (404 sender_not_found / 403 sender_not_verified). |
from | string | optional | Sender address on one of your verified domains (validated; 403 sender_not_verified otherwise). Ignored when sender_id is given. Omit both to use your workspace default sender. |
reply_to | string | string[] | optional | Reply-To address — a single email address or an array of addresses — set on the message. Precedence: this field, then the resolved sender's default reply_to (set per sender in Settings → Senders), then none. An invalid address is rejected as invalid_field (param reply_to) and nothing is sent. |
attachments | object[] | optional | Up to 20 attachments, each inline ({ filename, content (base64), content_type? }) or hosted ({ filename, url, content_type? }). See Attachments below. |
tracking | "full" | "essential" | "off" | optional | Per-message open-tracking override. Omit to inherit the workspace default (Settings → Workspace). "full" = precise opens + clicks; "essential" = day-only opens, clicks unaffected; "off" = no open/click tracking. "off" is gated — until your account is provisioned for it, requesting "off" returns 503 service_unavailable and nothing is sent. See the Open tracking guide. |
Example request body:
Reply-To. Pass reply_to as a single email or an array of emails to set the message's Reply-To. Precedence is request reply_to → the resolved sender's default reply_to → none; you can set a per-sender default under Settings → Senders. An invalid address fails with invalid_field (param: "reply_to") and the email is not sent.
Add up to 20 attachments. Each is either inline (base64 content) or hosted (a url we fetch at send time and discard — nothing is stored). Provide exactly one of content or url per attachment.
Host large files, attach small ones. Inline base64 is capped well below the request-body limit (a few MB) — for anything larger, host the file and pass a url. It's lighter on the wire and the recommended path for big files. See Deliverability for why.
Limits & errors. An oversized inline attachment, a disallowed file type, or an unreachable / disallowed URL is rejected with invalid_field (param: "attachments") and nothing is sent. A whole message past the ceiling of the provider that sends your domain is rejected with message_too_large (413); the error states the limit that applied. Treat 20 MB encoded as the figure you can rely on — some domains route through a provider that allows more, and none allow less. A large-but-sendable message succeeds and returns a soft warnings array nudging you to host the file instead.
On success the API returns 200 with a JSON body of success, id, and message. The id is the email's raw UUID — pass it to GET /api/v1/emails/{id} to read its delivery status.
Every live email passes a quick security check before it goes out. A new template, or one that names a brand that isn't yours, is checked once and then remembered, so the rest of your stream is not slowed. When the check holds an email, the API returns 202 with status: "held": the email has not been sent yet, it keeps its id, and it goes out after a quick security review (GET /api/v1/emails/{id} shows held until then). Later emails of the same template wait with it. An email that is not cleared is not sent and does not count toward your allowance.
On validation failure the API returns 400 with the standard error envelope (for example, a missing field):
Every response — success or error — also includes an X-Request-Id header. See Error codes for the canonical envelope and the full list of possible errors.
Testing? A ps_test_ key sends through the same path but captures instead of delivering — free, no reputation impact. See Test mode.
Every API key is limited to 1,000 requests per minute; over the limit you get a 429 rate_limited with a Retry-After header. To send in bulk, use Batch send rather than looping over /send. See Rate limits for the headers and handling.