PristineSend
Get started
API Reference

Send email

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.

Endpoint

POST/api/v1/send

Request headers

HeaderValue
AuthorizationBearer ps_live_YOUR_API_KEY — required
Content-Typeapplication/json — required

Request body

JSON object with the following fields:

FieldTypeRequiredDescription
tostringrequiredRecipient email address.
subjectstringrequiredEmail subject line.
htmlstringrequiredHTML body of the email.
sender_idstringoptionalID 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).
fromstringoptionalSender 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_tostring | string[]optionalReply-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.
attachmentsobject[]optionalUp to 20 attachments, each inline ({ filename, content (base64), content_type? }) or hosted ({ filename, url, content_type? }). See Attachments below.

Example request body:

{
  "to": "recipient@example.com",
  "subject": "Your order has shipped!",
  "html": "<h1>It's on the way</h1><p>Track your order at...</p>",
  "from": "orders@yourdomain.com",
  "reply_to": "support@yourdomain.com"
}

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.

Attachments

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.

{
  "to": "recipient@example.com",
  "subject": "Your invoice",
  "html": "<p>Invoice attached.</p>",
  "attachments": [
    { "filename": "invoice.pdf", "content": "JVBERi0xLjQK...", "content_type": "application/pdf" },
    { "filename": "report.csv", "url": "https://files.yourapp.com/report.csv" }
  ]
}

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 provider ceiling (~38 MB encoded) is rejected with message_too_large (413). A large-but-sendable message succeeds and returns a soft warnings array nudging you to host the file instead.

Response

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.

{
  "success": true,
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Email sent successfully"
}

On validation failure the API returns 400 with the standard error envelope (for example, a missing field):

{
  "error": {
    "code": "missing_field",
    "message": "Field 'to' is required.",
    "param": "to",
    "request_id": "req_8f3c9a1b2d4e5f6071829304"
  }
}

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.

Code examples

"color:#ff7b72">import { PristineSend } "color:#ff7b72">from "pristinesend"

"color:#ff7b72">const ps = "color:#ff7b72">new PristineSend(process.env.PRISTINESEND_API_KEY!)

"color:#ff7b72">const { id } = "color:#ff7b72">await ps.emails.send({
  to: "recipient@example.com",
  "color:#ff7b72">from: "orders@yourdomain.com", // omit to use your workspace default sender
  reply_to: "support@yourdomain.com", // or an array; falls back to the sender"color:#a5d6ff">'s default
  subject: "Your order has shipped!",
  html: "<h1>It's on the way</h1>",
  attachments: [
    // inline (small files) — base64 content
    { filename: "receipt.pdf", content: pdf.toString("base64"), content_type: "application/pdf" },
    // hosted (large files) — fetched at send time, never stored
    { filename: "manifest.csv", url: "https://files.yourapp.com/manifest.csv" },
  ],
})
// id is the email's UUID — read its status with ps.emails.get(id).
console.log("Sent:", id)

Testing? A ps_test_ key sends through the same path but captures instead of delivering — free, no reputation impact. See Test mode.

Rate limits

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.