← Back to docs

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);
}
Attachment limits

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" },
});
Template or inline — not both

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
}
Delivery tracking

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.