Sending Emails
Complete guide to sending transactional emails with the Postscale Node.js, Python, Ruby, and PHP SDKs, plus the REST API.
Use this page when you are ready to send through a verified outbound domain. The request fields below match the REST API body, and the official SDKs keep the same field names across Node.js, Python, Ruby, PHP, and raw HTTP requests.
Before You Send
- Your
fromaddress uses a domain configured asoutboundorboth. - The domain is active and verified.
- SPF is verified for the sending domain.
- An active DKIM key is verified.
- Your server has access to a Postscale API key.
Outbound-only domains do not need root MX records. If your business mailbox is hosted at Google Workspace, Microsoft 365, or another provider, keep those MX records in place and configure only the outbound DNS records. See Outbound Service for the full DNS and bounce setup.
Basic Send
Install one official SDK, or use cURL with the REST API:
npm install @postscale/postscale
Send your first email:
import { Postscale } from "@postscale/postscale";
const postscale = new Postscale(process.env.POSTSCALE_API_KEY);
const { data, error } = await postscale.emails.send({
from: "notifications@yourapp.com",
to: ["user@example.com"],
subject: "Your order has shipped",
html_body: "<p>Your order #12345 is on its way!</p>",
});
if (error) {
throw new Error(error.message);
}
console.log(data?.message_id);
Multiple Recipients
Send to multiple recipients at once:
await postscale.emails.send({
from: "newsletter@yourapp.com",
to: ["user1@example.com", "user2@example.com"],
subject: "Weekly Update",
html_body: "<p>Here is your weekly digest...</p>",
});
Each accepted recipient consumes one unit of your shared monthly email quota.
Recipients in to, cc, and bcc are counted separately. For example, one API
request with one to, one cc, and one bcc recipient counts as three emails.
Accepted inbound messages use the same monthly quota.
Plain Text Fallback
Always include a plain text version alongside HTML for maximum deliverability. Some email clients and spam filters prefer plain text, and it's required by certain providers.
await postscale.emails.send({
from: "notifications@yourapp.com",
to: ["user@example.com"],
subject: "Your order has shipped",
html_body: "<p>Your order #12345 is on its way!</p>",
text_body: "Your order #12345 is on its way!",
});
Attachments
Include file attachments by providing base64-encoded content, a filename, and the MIME content type:
import { attachmentFromFile } from "@postscale/postscale";
await postscale.emails.send({
from: "invoices@yourapp.com",
to: ["customer@example.com"],
subject: "Invoice #1234",
html_body: "<p>Please find your invoice attached.</p>",
text_body: "Please find your invoice attached.",
attachments: [
await attachmentFromFile("./invoice-1234.pdf", "application/pdf"),
await attachmentFromFile("./receipt.png", "image/png"),
],
});
Total message size (including attachments) must not exceed 50 MB. Base64 encoding increases file size by approximately 33%.
Sending with Templates
Instead of writing inline HTML, you can reference a saved template by slug or ID and pass variables:
await postscale.emails.send({
from: "orders@yourapp.com",
to: ["customer@example.com"],
template: "order-confirmation",
variables: {
first_name: "Jane",
order_id: "12345",
items: [
{ name: "Widget", price: "$29.99" },
{ name: "Gadget", price: "$49.99" },
],
total: "79.98",
currency: "quot;,
},
});
The template's subject, HTML body, and text body are rendered with your variables using Handlebars syntax ({{variable}}). Built-in variables like {{current_year}} and {{current_date}} are injected automatically.
You can also reference a template by UUID:
await postscale.emails.send({
from: "orders@yourapp.com",
to: ["customer@example.com"],
template_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
variables: { first_name: "Jane" },
});
You cannot combine template/template_id with inline subject/html_body/text_body. The API will return a 400 error if you set both.
See Templates for full documentation on creating templates, variable syntax, and the preview API.
Tags and Metadata
Add tags and metadata to organize and track your emails:
await postscale.emails.send({
from: "notifications@yourapp.com",
to: ["user@example.com"],
subject: "Welcome aboard!",
html_body: "<p>Welcome to the platform!</p>",
tags: ["welcome", "onboarding"],
metadata: {
user_id: "12345",
campaign: "welcome-series",
},
});
Tags and metadata appear in webhook payloads and the dashboard, making it easy to filter and analyze your sending activity.
Custom Headers
Use the headers request field to add custom message headers. Send them as a
JSON object whose keys are header names and whose values are strings:
await postscale.emails.send({
from: "notifications@yourapp.com",
to: ["user@example.com"],
subject: "Welcome aboard!",
html_body: "<p>Welcome to the platform!</p>",
headers: {
"X-Customer-ID": "12345",
"X-Campaign": "welcome",
"Reply-To": "support@example.com",
},
});
Do not send headers as an array of { name, value } objects. Message-ID is
managed by Postscale and custom Message-ID values are ignored.
Response
A successful send returns:
{
"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890@yourapp.com",
"status": "queued",
"warming_phase": "phase_1",
"remaining_today": 1500,
"remaining_this_hour": 100
}
Use webhooks to track delivered, bounced, deferred, and complained events as Postscale processes delivery logs, DSNs, and feedback-loop reports.
Bounce Tracking
Postscale records immediate SMTP rejections and deferrals from delivery logs. For delayed bounces, publish the return-path CNAME shown on your domain:
ps-bounces.yourapp.com. CNAME bounces.postscale.io.
With that record verified, delayed DSNs are delivered back to Postscale,
correlated to the original message, and emitted as email.bounced events. The
return-path CNAME is not a root MX record and does not make Postscale receive
normal mail for your domain.