Sending
PushMail sends emails via 10 supported providers. Use our managed infrastructure or bring your own API key for full control over your sending domain and reputation.
How sending works
API request or cron trigger
│
├── 1. Validate contact (subscribed + not suppressed)
├── 2. Check org has sufficient credits
├── 3. Render template with contact variables
├── 4. Store rendered HTML in R2 (for log viewing)
├── 5. Create send record (status: queued)
├── 6. Add to email queue
│
Queue consumer picks up:
│
├── 7. Resolve from address (per-request fromEmail or sending config)
├── 8. Send via provider API (SendGrid, Postmark, SES, etc.)
├── 9. Deduct credits from org balance
└── 10. Update monthly usage recordFrom address resolution
For transactional sends (POST /v1/send and POST /v1/send/batch), you can pass fromEmail and fromName directly in the request. This is the simplest way to send — no sending config required.
For sequences and campaigns, a sendingConfigId is required because these are automated sends with no per-request context.
Priority order:
fromEmail/fromNamefrom the API request (transactional only)- Sending config specified by
sendingConfigId - If neither is provided, the send will fail
Specifying the sender
The sender address field is called fromEmail (not from). You have two options for specifying who the email is sent from:
fromEmailand optionallyfromName— pass the sender address directly in the request. This uses the managed SendGrid infrastructure. Simplest option for getting started.sendingConfigId— reference a pre-configured sending config that includes the provider, credentials, and from address. Required for BYOK providers.
You must provide one of fromEmail or sendingConfigId. If both are provided, fromEmail takes priority for transactional sends. For sequences and campaigns, only sendingConfigId is supported.
Auto BCC
You can automatically BCC an email address on every send from a site or sequence. This is useful for CRM integration, compliance archiving, or internal monitoring.
- Site-level — set
autoBccon a site to BCC all emails (transactional, campaign, and sequence) sent from that site. - Sequence-level — set
autoBccon a sequence to override the site-level BCC for emails in that sequence.
curl -X PUT https://pushmail.dev/api/v1/sites/1 \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"autoBcc": "archive@yourcompany.com"}'curl -X PUT https://pushmail.dev/api/v1/sequences/1 \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"autoBcc": "sales-team@yourcompany.com"}'Set autoBcc to null to clear it. The BCC is applied at send time by the queue consumer and works with all 10 supported providers.
Single send
Send a transactional email with POST /v1/send. Pass fromEmail directly — no sending config setup needed.
curl -X POST https://pushmail.dev/api/v1/send \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"siteId": 1,
"fromEmail": "hello@yourdomain.com",
"fromName": "Your Company",
"subject": "Welcome!",
"html": "<h1>Hey {{first_name}}!</h1><p>Welcome aboard.</p>",
"variables": { "first_name": "Alice" }
}'Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Recipient email address |
siteId | integer | Yes | The site to send from |
fromEmail | string | One of fromEmail or sendingConfigId | Sender email address |
fromName | string | No | Sender display name |
sendingConfigId | integer | One of fromEmail or sendingConfigId | Sending config to use (required for BYOK) |
templateId | integer | One of templateId or html+subject | Template to render |
html | string | One of templateId or html+subject | Inline HTML body |
subject | string | Required with html | Subject line |
variables | object | No | Template variable substitution |
contactId | integer | No | Existing contact ID (auto-created if omitted) |
scheduledAt | string | No | ISO 8601 datetime for scheduled delivery |
secure | boolean | No | Send as encrypted secure message |
Batch sending
Send up to 1,000 emails in a single API call using POST /v1/send/batch. Each recipient gets individually tracked sends with per-recipient variable substitution. This dramatically reduces latency and API call overhead for high-volume transactional use cases like order confirmations, OTPs, and notifications.
curl -X POST https://pushmail.dev/api/v1/send/batch \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"templateId": 5,
"fromEmail": "hello@yourdomain.com",
"fromName": "Your Company",
"recipients": [
{
"to": "alice@example.com",
"variables": { "first_name": "Alice", "order_id": "ORD-001" }
},
{
"to": "bob@example.com",
"variables": { "first_name": "Bob", "order_id": "ORD-002" },
"tags": ["vip"]
},
{
"to": "carol@example.com",
"variables": { "first_name": "Carol", "order_id": "ORD-003" }
}
]
}'{
"data": {
"queued": 3,
"rejected": 0,
"estimatedCostCents": 1,
"sends": [
{ "to": "alice@example.com", "sendId": 101, "status": "queued" },
{ "to": "bob@example.com", "sendId": 102, "status": "queued" },
{ "to": "carol@example.com", "sendId": 103, "status": "queued" }
]
}
}You can also use inline HTML instead of a template:
curl -X POST https://pushmail.dev/api/v1/send/batch \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"subject": "Your order confirmation",
"html": "<h1>Thanks {{first_name}}!</h1><p>Order {{order_id}} confirmed.</p>",
"recipients": [
{ "to": "alice@example.com", "variables": { "first_name": "Alice", "order_id": "ORD-001" } },
{ "to": "bob@example.com", "variables": { "first_name": "Bob", "order_id": "ORD-002" } }
]
}'Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
siteId | integer | Yes | The site to send from |
templateId | integer | One of templateId or html+subject | Template to render for each recipient |
html | string | One of templateId or html+subject | Inline HTML body (supports {{variable}} substitution) |
subject | string | Required with html | Subject line (required when using inline HTML) |
fromEmail | string | One of fromEmail or sendingConfigId | Sender email address (e.g. hello@yourdomain.com) |
fromName | string | No | Sender display name (e.g. Your Company) |
sendingConfigId | integer | One of fromEmail or sendingConfigId | Sending config to use (required for BYOK providers) |
recipients | array | Yes | Array of recipient objects (1--1,000 items) |
recipients[].to | string | Yes | Recipient email address |
recipients[].variables | object | No | Key-value pairs for template variable substitution |
recipients[].tags | string[] | No | Tags to apply to this contact |
Batch behavior
- Credit check: Credits are verified upfront for the entire batch before any sends are queued. If your balance is insufficient, the entire batch is rejected with a
402status. - Partial success: If some recipients fail validation (bad email, suppressed, duplicate), the valid recipients are still queued. The response includes per-recipient status.
- Deduplication: Duplicate emails within a single batch are automatically rejected (case-insensitive).
- Suppression: Each recipient is checked against the global bounce/complaint suppression list.
- Tags: Per-recipient tags are created and applied to contacts automatically.
- Tracking: Each recipient gets an individual send record with its own
sendIdfor event tracking. - Contact creation: If a recipient email does not have an existing contact record, one is created automatically with source
transactional.
Batch error responses
| Status | Condition | Example |
|---|---|---|
400 | Invalid request body, missing required fields, empty recipients, or over 1,000 recipients | { "error": "At least one recipient is required" } |
401 | Missing or invalid API key / session | { "error": "Unauthorized" } |
402 | Insufficient credits for the batch | { "error": "Insufficient credits. This batch requires approximately 30 cents but your balance is 5 cents." } |
404 | Site or template not found, or not owned by your org | { "error": "Site not found" } |
When some recipients are rejected but others succeed, the endpoint returns 202 with per-recipient status:
{
"data": {
"queued": 2,
"rejected": 1,
"estimatedCostCents": 1,
"sends": [
{ "to": "alice@example.com", "sendId": 101, "status": "queued" },
{ "to": "bad@invalid.test", "sendId": null, "status": "rejected", "reason": "Domain has no mail server" },
{ "to": "bob@example.com", "sendId": 102, "status": "queued" }
]
}
}When all recipients are rejected, the endpoint returns 200 with zero queued:
{
"data": {
"queued": 0,
"rejected": 3,
"sends": [
{ "to": "dup@example.com", "sendId": null, "status": "rejected", "reason": "Duplicate recipient in batch" },
{ "to": "dup@example.com", "sendId": null, "status": "rejected", "reason": "Duplicate recipient in batch" },
{ "to": "bounced@example.com", "sendId": null, "status": "rejected", "reason": "Previously bounced" }
]
}
}Batch limits
| Limit | Value |
|---|---|
| Max recipients per batch | 1,000 |
| Max request body size | 10 MB |
Scheduled sends
Schedule a transactional email to be sent at a future time by passing a scheduledAt ISO 8601 datetime to POST /v1/send. The cron worker processes scheduled sends every minute.
curl -X POST https://pushmail.dev/api/v1/send \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"siteId": 1,
"templateId": 5,
"variables": { "first_name": "Alice" },
"scheduledAt": "2026-03-01T09:00:00Z"
}'{
"data": {
"sendId": 42,
"status": "scheduled",
"scheduledAt": "2026-03-01T09:00:00Z"
}
}Status flow: scheduled → queued → sent → delivered
If scheduledAt is omitted, the email is queued immediately as before.
Managed vs BYOK
| Managed (default) | BYOK (Bring Your Own Key) | |
|---|---|---|
| Provider | PushMail's managed SendGrid account | Your own account with any supported provider |
| From domain | Sent via PushMail domain | Your verified domain |
| Reputation | Shared IP reputation | Your own IP/domain reputation |
| Setup | None — works immediately | Provide API key + verify domain |
| Cost | PushMail per-email pricing only | PushMail pricing + your provider costs |
| Best for | Getting started quickly | Production apps needing branded sending |
Multi-provider support
BYOK sending configs support 10 email providers. Use whichever provider you already have an account with -- or create configs for multiple providers and choose per-campaign, per-sequence, or per-send.
| Provider | ID |
|---|---|
| SendGrid | sendgrid |
| Amazon SES | ses |
| Postmark | postmark |
| Mailgun | mailgun |
| SparkPost | sparkpost |
| Brevo (Sendinblue) | brevo |
| Mailjet | mailjet |
| Mandrill (Mailchimp Transactional) | mandrill |
| Resend | resend |
| Elastic Email | elastic-email |
See Providers for per-provider setup details, required config fields, and webhook configuration.
Set up BYOK sending
Add your provider API key and configure a from address. PushMail will use your key for all sends from your organization.
curl -X POST https://pushmail.dev/api/v1/sending-configs \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "sendgrid",
"apiKey": "SG.your-sendgrid-api-key",
"fromEmail": "hello@yourdomain.com",
"fromName": "Your Company"
}'The provider field accepts any of the 10 supported provider IDs listed above. Each provider requires different config fields -- use GET /v1/providers to see the required fields for each.
curl -X POST https://pushmail.dev/api/v1/sending-configs/1/verify \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "you@yourdomain.com" }'
# Sends a test email to the address you specifyPricing
Pay per email with volume discounts. Every new account starts with $5.00 in free credits.
| Monthly volume | Per email | $10 gets you |
|---|---|---|
| 0 — 10,000 | $0.003 (0.3¢) | ~3,333 emails |
| 10,001 — 100,000 | $0.002 (0.2¢) | ~5,000 emails |
| 100,001 — 1,000,000 | $0.001 (0.1¢) | ~10,000 emails |
| 1,000,001+ | $0.0005 (0.05¢) | ~20,000 emails |
Credits are deducted at the moment of send. Cost per email depends on your total sends that month — as volume goes up, cost per email goes down.
Branding discount — save 15%
Enable the "Sent with PushMail" footer and get 15% off every email. The footer adds a small, tasteful link at the bottom of your emails:
Sent with PushMailToggle it on in Settings > Branding Discount or via the API:
curl -X PUT https://pushmail.dev/api/v1/org/branding \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'curl https://pushmail.dev/api/v1/org/branding \
-H "Authorization: Bearer pm_live_YOUR_KEY"The discount is applied automatically at send time. You can disable it anytime — no commitment, no lock-in.
Template variables
Templates support variable substitution with {{variable_name}} syntax. Variables are replaced at send time with contact data.
| Variable | Value |
|---|---|
{{email}} | Contact's email address |
{{first_name}} | Contact's first name |
{{last_name}} | Contact's last name |
{{full_name}} | First + last name combined |
{{custom_field_name}} | Any custom field set on the contact |
<h1>Hey {{first_name}},</h1>
<p>Welcome to {{company}}! Here are your next steps...</p>Unmatched variables are removed from the output. All values are HTML-escaped to prevent injection.
Conditional content
Show or hide sections of your email based on contact attributes using {{#if}} blocks. Conditionals are processed before variable substitution, so you can use {{variable}} tags inside conditional blocks.
Basic if block
Show content only when a variable exists and is truthy (not empty, not "false", not "0").
{{#if first_name}}
<h1>Hi {{first_name}}!</h1>
{{/if}}If/else block
Provide fallback content when the condition is false.
{{#if first_name}}
<h1>Hi {{first_name}}!</h1>
{{#else}}
<h1>Hi there!</h1>
{{/if}}Comparison operators
Compare a variable against a specific value using an operator.
{{#if plan_type equals "pro"}}
<p>Thanks for being a Pro member!</p>
{{/if}}
{{#if purchase_count gt "5"}}
<p>You're one of our top customers!</p>
{{/if}}Supported operators:
| Operator | Description | Example |
|---|---|---|
equals | Exact string match | {{#if plan_type equals "pro"}} |
not_equals | String does not match | {{#if status not_equals "inactive"}} |
contains | String contains substring | {{#if email contains "gmail"}} |
gt | Numeric greater than | {{#if purchase_count gt "5"}} |
lt | Numeric less than | {{#if age lt "18"}} |
exists | Variable is defined (even if empty) | {{#if phone exists}} |
not_exists | Variable is not defined | {{#if phone not_exists}} |
Values must be wrapped in double or single quotes: "value" or 'value'. The gt and lt operators parse both sides as numbers.
Nested conditionals
You can nest conditional blocks at least 2 levels deep.
{{#if first_name}}
Hi {{first_name}}
{{#if plan_type equals "pro"}}
(Pro member)
{{#else}}
(Free tier)
{{/if}}
!
{{#else}}
Hello!
{{/if}}Truthiness rules
A variable is considered falsy (and the {{#else}} branch is used) when:
- The variable does not exist in the contact data
- The variable is an empty string
"" - The variable is the string
"false" - The variable is the string
"0"
Everything else is truthy.
Full example
{{#if first_name}}
<h1>Hey {{first_name}},</h1>
{{#else}}
<h1>Hey there,</h1>
{{/if}}
<p>Welcome to {{company}}!</p>
{{#if purchase_count gt "5"}}
<p>You're one of our top customers! Here's an exclusive offer.</p>
{{/if}}
{{#if referral_code}}
<p>Share your referral code: {{referral_code}}</p>
{{/if}}
{{#if email contains "gmail"}}
<p>Pro tip: Add us to your Primary tab so you never miss an update.</p>
{{/if}}Conditional blocks work in both HTML and plain text templates. The conditional tags themselves ({{#if}}, {{#else}}, {{/if}}) are completely removed from the output.
View sent emails
Every sent email's rendered HTML is stored in R2. You can retrieve it to see exactly what the recipient saw.
curl "https://pushmail.dev/api/v1/sends?siteId=1&page=1&limit=20" \
-H "Authorization: Bearer pm_live_YOUR_KEY"CAN-SPAM compliance
PushMail automatically handles CAN-SPAM compliance on every send. When enabled (on by default):
- List-Unsubscribe header — RFC 8058 one-click unsubscribe, shown as "Unsubscribe" button in Gmail, Apple Mail, etc.
- Physical address footer — Your mailing address appended to every email body
- Unsubscribe link — A visible opt-out link in the footer
Set your physical address in Settings > Compliance or via the API:
curl -X PUT https://pushmail.dev/api/v1/compliance \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"physicalAddress": "123 Main St, Suite 100, San Francisco, CA 94105",
"complianceEnabled": true
}'See CAN-SPAM Compliance for full documentation.
Automatic suppression
PushMail automatically suppresses contacts that bounce or report spam. Suppressed contacts will not receive any emails from any sequence or campaign.
- Hard bounce: Contact status set to
bounced, added to suppression cache for 365 days - Spam report: Contact status set to
complained, added to suppression cache for 365 days - Unsubscribe: Contact status set to
unsubscribed, added to suppression cache for 365 days
Secure messages
Send encrypted emails with secure: true. The content is encrypted with AES-256-GCM before storage, and recipients decrypt it in their browser using a passphrase. Senders can revoke messages at any time, and expired messages are automatically cleaned up.
curl -X POST https://pushmail.dev/api/v1/send \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"siteId": 1,
"subject": "Confidential",
"html": "<p>Sensitive content here</p>",
"secure": true,
"expiresInHours": 48
}'See Secure Messages for full documentation on encryption, revocation, and message lifecycle.
Next steps
- Secure Messages — End-to-end encrypted email with revocation
- Webhooks — React to delivery events in your app
- API Reference — Full endpoint documentation
Forms
Create embeddable signup forms that capture contacts, apply tags, add to lists, and enroll in sequences automatically.
Email Providers
PushMail supports 10 email providers via BYOK (Bring Your Own Key). Connect your own SendGrid, SES, Postmark, Mailgun, or any supported provider to send through your own infrastructure.