Posthawk CLI
The Posthawk CLI ships inside the same npm package as the Node.js SDK. A single global install gives you commands to scaffold projects, preview React Email templates with hot reload, send mail from the terminal, and manage your whole account — domains, contacts, suppressions, webhooks, broadcasts, newsletters, scheduled emails, and validation. There is also an interactive REPL (run bare `posthawk` in a TTY) and an interactive send wizard.
npm i -g posthawkInstall once globally. You get the `posthawk` binary plus all the SDK types. The CLI works on macOS, Linux, and Windows with Node.js 18 or newer.
npm i -g posthawk
# verify install
posthawk --versionposthawk loginStore your API key locally in ~/.posthawk/config.json (file mode 0600). The CLI verifies the key against /v1/domains before saving. You can also set POSTHAWK_API_KEY as an env var to override the stored key.
# Interactive login (masked prompt)
posthawk login
# Or via environment variable
export POSTHAWK_API_KEY=ck_live_...
# Check the active key
posthawk whoami
# Remove the stored key
posthawk logoutposthawk init [dir]Scaffold a new project with a React Email starter template, package.json, .env.local, and .gitignore. Run it in an existing directory to add the files without overwriting anything.
# New folder
posthawk init my-emails
cd my-emails
npm install
# Or scaffold into the current folder
posthawk init .
# Output:
# created emails/welcome.tsx
# created .env.local
# created .gitignore
# created package.json
# ✓ Project ready.posthawk preview <file.tsx>Start a local dev server with hot reload for a React Email template. Edit the file and the rendered preview updates instantly via SSE. The preview server has an HTML view and a Plain Text view so you can check both variants of the email.
Parameters
filestringrequiredPath to a .tsx/.jsx email template with a default export
--portnumberoptionalOverride the default port (7321)
--propsjsonoptionalJSON string passed as props to the component
# Start the preview server
posthawk preview emails/welcome.tsx
# Custom port
posthawk preview emails/welcome.tsx --port 8000
# Pass props to the component
posthawk preview emails/welcome.tsx \
--props '{"name":"Alex","actionUrl":"https://example.com"}'
# Output:
# ▸ starting dev server at http://localhost:7321
# ✓ ready in 182ms
# watching for changes · press Ctrl+C to stopposthawk send [file.tsx] --to <email> --from <email>Send an email from the terminal. Supply exactly one body source: a React Email template file (compiled + rendered to HTML/text), inline --html, inline --text, or a --template-id. The file positional is OPTIONAL. In a TTY, running `posthawk send` with no file and no --html/--text/--template-id launches an interactive wizard (pick a verified domain, recipient, subject, body type, and confirm).
Parameters
filestringoptionalPath to a .tsx/.jsx email template. Optional — omit to use --html/--text/--template-id or the interactive wizard.
--tostringrequiredRecipient(s), comma-separated
--fromstringrequiredSender (must be from a verified domain)
--subjectstringoptionalSubject line (required for inline --html/--text sends; overrides a template's default subject)
--ccstringoptionalCC recipients, comma-separated
--bccstringoptionalBCC recipients, comma-separated
--textstringoptionalInline plain-text body (use instead of a template file)
--htmlstringoptionalInline HTML body (use instead of a template file)
--template-idstringoptionalSend using a stored Posthawk template by id
--reply-tostringoptionalReply-to address
--scheduled-forstringoptionalISO 8601 datetime to schedule the send
--timezonestringoptionalIANA timezone for --scheduled-for
--idempotency-keystringoptionalIdempotency-Key header for safe retries
--propsjsonoptionalJSON string passed as props to the template component
--dry-runflagoptionalCompile and render without sending
# Template file send
posthawk send emails/welcome.tsx \
--to alex@acme.io \
--from hello@yourdomain.dev
# Inline HTML send (no file)
posthawk send \
--to alex@acme.io \
--from hello@yourdomain.dev \
--subject "Quick note" \
--html "<h1>Hello!</h1>"
# Send a stored template, scheduled for later
posthawk send \
--template-id welcome-template \
--to alex@acme.io \
--from hello@yourdomain.dev \
--scheduled-for 2026-06-20T14:00:00Z \
--timezone America/New_York \
--idempotency-key 550e8400-e29b-41d4-a716-446655440000
# Interactive wizard (TTY, no file/body flags)
posthawk send
# Dry-run: compile and render without hitting the API
posthawk send emails/welcome.tsx \
--to test@example.com \
--from hi@example.dev \
--dry-runexport default Component; export const subjectThe CLI expects each template to default-export a React component. Optionally, you can export a `subject` string or function — the CLI uses it as the default subject when --subject is not passed. The `subject` function receives the same props as the component.
// emails/welcome.tsx
import { Body, Container, Heading, Html, Text } from '@react-email/components';
interface WelcomeEmailProps {
name?: string;
}
// Optional subject export — receives the same props as the component
export const subject = ({ name }: WelcomeEmailProps) =>
`Welcome to Posthawk${name ? `, ${name}` : ''}!`;
// Default export is the component
export default function WelcomeEmail({ name = 'there' }: WelcomeEmailProps) {
return (
<Html>
<Body>
<Container>
<Heading>Hi {name},</Heading>
<Text>Welcome to Posthawk!</Text>
</Container>
</Body>
</Html>
);
}posthawk validate <email> · posthawk status <jobId>Validate a single email address, or check the delivery status of a previously sent email by its job id. Both take a single required positional argument and no flags.
# Validate an address
posthawk validate user@example.com
# Check a send's status
posthawk status abc-123-defposthawk domains [list|get|add|verify|remove|receiving|webhook]Manage sending domains. Bare `posthawk domains` defaults to list. Subcommands cover add/verify/remove, inbound receiving toggles, and webhook config.
# List (default) — shows verification status + region
posthawk domains
posthawk domains list
# Get one domain's DNS records + state
posthawk domains get <id>
# Add a domain (optional --region: us-east-1 | eu-north-1)
posthawk domains add mail.example.com --region us-east-1
# Verify after DNS propagates
posthawk domains verify <id>
# Remove a domain
posthawk domains remove <id>
# Inbound receiving
posthawk domains receiving enable <id>
posthawk domains receiving disable <id>
# Inbound webhook
posthawk domains webhook set <id> --url https://yourapp.com/inbound
posthawk domains webhook test <id>posthawk scheduled [list|get|cancel|reschedule|send-now]Manage scheduled emails. Bare `posthawk scheduled` defaults to list. reschedule takes an --at ISO datetime.
# List (default) — filterable + paginated
posthawk scheduled
posthawk scheduled list --status scheduled --limit 20 --offset 0
# Get one
posthawk scheduled get <id>
# Cancel
posthawk scheduled cancel <id>
# Reschedule to a new time
posthawk scheduled reschedule <id> --at 2026-06-20T14:00:00Z
# Send immediately
posthawk scheduled send-now <id>posthawk contacts [list|get|create|update|delete|import]Manage workspace contacts from the terminal. Bare `posthawk contacts` defaults to list. import reads a JSON file (an array, or an object with a "contacts" array).
# List (default) — optional filters
posthawk contacts
posthawk contacts list --tag newsletter --search jane --page 1
# Get one
posthawk contacts get <id>
# Create
posthawk contacts create --email jane@example.com --name "Jane" --tags newsletter,beta
# Update (at least one field)
posthawk contacts update <id> --tags newsletter,premium
posthawk contacts update <id> --unsubscribed true
# Delete
posthawk contacts delete <id>
# Bulk import from a JSON file
posthawk contacts import contacts.jsonposthawk suppressions [list|add|remove]Manage the suppression list. Bare `posthawk suppressions` defaults to list. add takes an email positional and an optional --notes.
# List (default)
posthawk suppressions
posthawk suppressions list
# Add (reason "manual")
posthawk suppressions add user@example.com --notes "Requested removal"
# Remove by id
posthawk suppressions remove <id>posthawk webhooks [list|create|update|delete|test]Manage webhook endpoints. Bare `posthawk webhooks` defaults to list. --events is a comma-separated list (send, delivery, bounce, complaint, reject, delivery_delay, open, click).
# List (default)
posthawk webhooks
# Create
posthawk webhooks create \
--url https://yourapp.com/webhooks/posthawk \
--events delivery,bounce,complaint \
--description "Production webhook"
# Update (at least one field)
posthawk webhooks update <id> --events delivery,bounce,complaint,open,click
posthawk webhooks update <id> --enabled false
# Test (sends a synthetic event)
posthawk webhooks test <id>
# Delete
posthawk webhooks delete <id>posthawk broadcasts [list|get|create|send|cancel]Manage one-off broadcasts. Bare `posthawk broadcasts` defaults to list. create uses --type all|tag with an optional --tag segment.
# List (default) — optional --status + --page
posthawk broadcasts
posthawk broadcasts list --status draft --page 1
# Get one
posthawk broadcasts get <id>
# Create
posthawk broadcasts create \
--name "March Update" \
--from hello@yourdomain.com \
--from-name "Acme" \
--subject "What shipped in March" \
--html "<h1>This month at Acme</h1>..." \
--type tag --tag newsletter
# Send / cancel
posthawk broadcasts send <id>
posthawk broadcasts cancel <id>posthawk watch <file.tsx> --to <email> --from <email>Watch a React Email template and re-send on every save — handy for iterating against a real inbox. Requires a file positional plus --to and --from.
Parameters
filestringrequiredPath to a .tsx/.jsx email template
--tostringrequiredRecipient(s), comma-separated
--fromstringrequiredSender (verified domain)
--subjectstringoptionalSubject line
--ccstringoptionalCC recipients
--bccstringoptionalBCC recipients
--propsjsonoptionalJSON props for the component
posthawk watch emails/welcome.tsx \
--to me@example.com \
--from hello@yourdomain.dev \
--props '{"name":"Alex"}'posthawk logs [--follow] [--limit <n>]Show your local CLI send history (recorded on this machine). --follow (alias -f) tails new entries; --limit caps how many rows are shown.
posthawk logs --limit 50
posthawk logs --followposthawk doctor · upgrade · completion <shell>Maintenance commands. doctor runs health checks (CLI version vs npm, auth, API reachability, project peer deps). upgrade runs `npm install -g posthawk@latest`. completion prints a shell completion script for bash, zsh, or fish.
# Diagnose your setup
posthawk doctor
# Upgrade the CLI to the latest release
posthawk upgrade
# Print a completion script
posthawk completion zsh # or: bash | fishposthawkRun `posthawk` with no arguments in a TTY to drop into an interactive REPL. The prompt accepts any CLI command (a leading `posthawk` token is stripped if you type it). Tab-completion is available. Built-ins: exit / quit / :q to leave, clear / cls to clear the screen.
$ posthawk
posthawk› domains list
posthawk› send --to me@example.com --from hi@yourdomain.dev --subject Hi --text "Hello"
posthawk› exitPOSTHAWK_API_KEY / POSTHAWK_BASE_URLAll CLI commands respect the same environment variables as the SDK. Env vars take precedence over the stored config file.
Parameters
POSTHAWK_API_KEYstringoptionalOverrides the stored API key — essential for CI/CD
POSTHAWK_BASE_URLstringoptionalOverrides the API URL — use this for self-hosted instances
NO_COLORflagoptionalDisables colored terminal output
# Run in CI without interactive login
export POSTHAWK_API_KEY=ck_live_...
posthawk send emails/release-notes.tsx \
--to announcements@acme.io \
--from release-bot@acme.dev \
--props "{\"version\":\"${GITHUB_REF_NAME}\"}"
# Self-hosted instance
export POSTHAWK_BASE_URL=https://api.yourdomain.com
posthawk whoamiGitHub Actions workflowShip release notes automatically when a git tag is pushed. The CLI works in any CI environment that has Node.js 18+ and can install global npm packages.
# .github/workflows/release-notes.yml
name: Send Release Notes
on:
push:
tags: ['v*']
jobs:
send:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm i -g posthawk
- name: Send release notes
env:
POSTHAWK_API_KEY: ${{ secrets.POSTHAWK_API_KEY }}
run: |
posthawk send emails/release-notes.tsx \
--to changelog@acme.io \
--from release-bot@acme.dev \
--subject "Acme ${{ github.ref_name }} shipped" \
--props "{\"version\":\"${{ github.ref_name }}\"}"esbuild + React EmailThe CLI compiles your .tsx file at runtime using esbuild, then dynamic-imports the result to render with @react-email/render. Compiled output is cached in node_modules/.posthawk-cache/ so subsequent runs start faster. React and @react-email/* are kept external during compilation so they resolve from your project's own node_modules — meaning you always get the exact same React Email version your templates were authored against.
# What the CLI does when you run 'posthawk send welcome.tsx':
#
# 1. esbuild bundles welcome.tsx → ESM with react/@react-email/*
# left as external imports
# 2. Writes the output to node_modules/.posthawk-cache/<hash>.mjs
# 3. Dynamic-imports that file with a cache-busting ?t=timestamp
# 4. Pulls the default export as the component
# 5. Calls render() from @react-email/render to get HTML + text
# 6. Sends the rendered payload via the SDK
#
# First compile: ~500ms cold. Subsequent compiles: ~200ms warm.