Sending Emails
Sending Emails
Postscale makes it easy to send transactional emails with high deliverability. This guide covers the main options available when sending emails. Examples use the official Node.js, Python, Ruby, and PHP SDKs, and request fields match the REST API body exactly.
Before You Send
Your from address must use a domain configured as outbound or both.
Sending is allowed only after the domain is active, verified, SPF is verified,
and an active DKIM key is verified.
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 an official SDK:
npm install @postscale/postscale
pip install postscale
gem install postscale
composer require postscale/postscale-php
The simplest way to send an email from server-side Node.js code:
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);
Python uses the same API-native request fields:
import os
from postscale import Postscale
postscale = Postscale(os.environ["POSTSCALE_API_KEY"])
result = 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 result.error:
raise RuntimeError(result.error.message)
print(result.data["message_id"])
Ruby uses the same API-native request fields:
require "postscale"
postscale = Postscale::Client.new(api_key: ENV.fetch("POSTSCALE_API_KEY"))
result = 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 result.error
raise result.error.message
end
puts result.data["message_id"]
PHP uses the same API-native request fields:
<?php
use Postscale\Postscale;
$postscale = Postscale::client(getenv("POSTSCALE_API_KEY"));
$result = $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 ($result->error !== null) {
throw new RuntimeException($result->error->message);
}
echo $result->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"),
],
});
Python:
from postscale import attachment_from_file
result = 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": [
attachment_from_file("./invoice-1234.pdf", "application/pdf"),
attachment_from_file("./receipt.png", "image/png"),
],
})
if result.error:
raise RuntimeError(result.error.message)
Ruby:
attachment = Postscale.attachment_from_file(
"./invoice-1234.pdf",
"application/pdf"
)
result = 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: [attachment]
)
if result.error
raise result.error.message
end
PHP:
use Postscale\Attachment;
$attachment = Attachment::fromFile(
"./invoice-1234.pdf",
"application/pdf",
);
$result = $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" => [$attachment],
]);
if ($result->error !== null) {
throw new RuntimeException($result->error->message);
}
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.
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.