Composing and Sending Newsletters
Step-by-step guide to creating and sending newsletters via dashboard or API
Composing and Sending Newsletters
This guide walks you through creating and sending newsletters using both the Penna dashboard and API.
Via Dashboard
Step 1: Start Composing
- Navigate to Dashboard → Compose or click Compose in the top navigation
- You'll see the newsletter composer interface
Step 2: Write Your Subject Line
Best practices:
- Keep under 50 characters
- Be specific and compelling
- Avoid spam trigger words
- Front-load important information (mobile truncates)
- Test different approaches
Examples:
- ✅ "New React hooks pattern you'll love"
- ✅ "This week's web dev insights"
- ❌ "Newsletter #47"
- ❌ "FREE MONEY NOW!!!"
Step 3: Compose Your Content
Markdown support:
Penna supports basic markdown formatting:
# Main Heading
## Subheading
Here's a paragraph with **bold text** and _italic text_.
- Bullet point 1
- Bullet point 2
[Link text](https://example.com)Formatting tips:
- Use headings to structure content
- Keep paragraphs short (2-3 sentences)
- Use bullet points for scanability
- Add links sparingly
- Test on mobile before sending
Step 4: Preview Your Newsletter
- Click Preview button
- Review how it will look to subscribers
- Check both desktop and mobile views
- Verify all links work
- Proofread for typos
Preview checklist:
- ✅ Subject line displays correctly
- ✅ Content formats properly
- ✅ Links all work
- ✅ Images load (if any)
- ✅ Looks good on mobile
- ✅ No typos or errors
Step 5: Select Recipients
Option 1: All Subscribers
- Send to your entire list
- Good for general announcements
- Maximum reach
Option 2: Specific Segments
- Target specific groups
- Better engagement
- More relevant content
- Can select multiple segments
Option 3: Individual Emails (Coming Soon)
- Send to specific addresses
- Good for testing
- Personal outreach
Example:
To: Web Development + React segments
Total recipients: 2,345 subscribersPenna automatically deduplicates if someone is in multiple segments.
Step 6: Send or Schedule
Send Now:
- Click Send button
- Confirm your selection
- Newsletter sends immediately
- Can't be undone
Schedule for Later: (Coming Soon)
- Choose date and time
- Timezone-aware sending
- Cancel before send time
- Automated delivery
Step 7: Monitor Results
After sending:
- Go to Analytics (Coming Soon)
- View delivery status
- Track opens and clicks
- Monitor engagement
- Learn for next time
Via API
Authentication
Send newsletters using both your Public and Private keys:
x-penna-public-key: penn_your_public_key
x-penna-private-key: pk_your_private_keyImportant: Never expose your Private Key in client-side code. Only use it from your backend server.
Basic Send Request
Endpoint: POST /api/v1/external/newsletters/send
Example:
const response = await fetch(
"https://api.penna.dev/api/v1/external/newsletters/send",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-penna-public-key": "penn_your_public_key",
"x-penna-private-key": "pk_your_private_key",
},
body: JSON.stringify({
subject: "Your newsletter subject",
content: "# Hello!\n\nYour content here...",
segmentIds: ["segment-id-1"],
}),
},
);
const result = await response.json();
console.log(result);Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | Email subject line |
content | string | Yes | Newsletter body (supports basic markdown) |
recipientEmails | string[] | No* | Specific email addresses |
segmentIds | string[] | No* | Segment IDs to send to |
* At least one of recipientEmails or segmentIds required
Sending to All Subscribers
To send to all subscribers, pass an empty segmentIds array:
{
subject: 'Newsletter for everyone',
content: 'Your content...',
segmentIds: []
}Sending to Specific Segments
Send to one or more segments:
{
subject: 'React developers update',
content: 'New React 19 features...',
segmentIds: [
'react-developers-id',
'web-dev-id'
]
}Sending to Specific Emails
Send to specific subscribers:
{
subject: 'Personal update',
content: 'Hey there...',
recipientEmails: [
'user1@example.com',
'user2@example.com'
]
}Note: recipientEmails must be actual subscribers. Non-subscribers are automatically skipped.
Combining Recipients
You can combine segments and specific emails:
{
subject: 'Big announcement',
content: 'Exciting news...',
segmentIds: ['pro-users'],
recipientEmails: ['special@example.com']
}The total recipients are merged and deduplicated automatically.
Markdown Support
Content supports basic markdown:
{
subject: 'Weekly update',
content: `
# This Week's Highlights
## New Features
We shipped dark mode!
## Tips
- Use keyboard shortcuts
- Enable notifications
- Try the new editor
Check it out: [link](https://example.com)
`
}Currently supported:
#,##,###headings- Blank lines (paragraph breaks)
- Basic text formatting
Coming soon:
- Lists, links, bold, italic
- Images
- Tables
- Code blocks
Success Response
{
"success": true,
"message": "Newsletter sent successfully",
"data": {
"skippedNonSubscribers": 0
}
}Error Responses
Missing Private Key:
{
"success": false,
"message": "Unauthorized: Private key required"
}Status: 401
No Recipients:
{
"success": false,
"message": "No recipients specified"
}Status: 400
Too Many Recipients:
{
"success": false,
"message": "This request resolved to 7000 recipients, which exceeds the 5000 limit per request."
}Status: 400
Daily Limit Reached:
{
"success": false,
"message": "Your newsletter has reached its professional plan limit of 20 newsletter sends per day."
}Status: 429
Content Moderation Block:
{
"success": false,
"message": "This newsletter was blocked by content moderation: spam detected"
}Status: 403
Rate Limits
Per-Request Limits
- Max recipients: 5,000 per request
- Max content length: 100KB
- API requests: 100 per hour per IP
Daily Send Limits
Limits by plan:
- Hobby: 3 newsletter sends/day
- Professional: 20 newsletter sends/day
- Business: 100 newsletter sends/day
- Enterprise: Unlimited
Note: A "newsletter send" is one broadcast to multiple recipients. Individual subscriber additions don't count toward this limit.
Handling Large Lists
For lists over 5,000 subscribers:
Option 1: Use segments
// Send to all by using empty segmentIds
{
subject: 'Update',
content: '...',
segmentIds: []
}Option 2: Batch sends
// Split into chunks of 5,000
const batches = chunkArray(subscribers, 5000);
for (const batch of batches) {
await sendNewsletter({
subject: "Update",
content: "...",
recipientEmails: batch,
});
// Small delay between batches
await sleep(1000);
}Content Moderation
All newsletters are automatically scanned for:
- Spam content
- Phishing attempts
- Scam language
- Hate speech
- Adult content
Legitimate newsletters pass automatically.
This is an anti-abuse check, not editorial review. Normal promotional content, product announcements, and marketing emails are fine.
If blocked:
- Review your content
- Remove any problematic language
- Retry
- Contact support if you believe it's an error
Testing Before Sending
Send Test Email
Dashboard:
- Compose your newsletter
- Click Send Test
- Enter test email addresses
- Review the test email
- Make adjustments
- Send live when ready
API:
// Send to your test email first
await sendNewsletter({
subject: "[TEST] Your subject",
content: "Your content...",
recipientEmails: ["yourtest@email.com"],
});
// Review, then send to full listTesting Checklist
Before sending to full list:
✅ Subject line is compelling
✅ Content provides clear value
✅ All links work
✅ Formatting looks good on mobile
✅ No typos or errors
✅ Unsubscribe link present (automatic)
✅ Images load properly (if any)
✅ Call-to-action is clear
✅ Preview text is set
✅ Correct recipients selected
Best Practices
Timing
Best days to send:
- Tuesday, Wednesday, Thursday (highest engagement)
- Monday (good for B2B)
- Weekend (depends on audience)
Best times:
- 10-11 AM (start of workday)
- 2-3 PM (post-lunch)
- 8-9 PM (evening)
Test different times and track what works for your audience.
Frequency
Common schedules:
- Weekly - Most popular, consistent
- Bi-weekly - Good balance
- Monthly - Risk being forgotten
- Daily - Only for time-sensitive content
Consistency matters more than frequency.
Subject Line Testing
A/B test subject lines:
Test 1:
- A: "New React tutorial"
- B: "The React pattern we use in production"
Test 2:
- A: "This week's roundup"
- B: "5 tools to boost your productivity"
Send each to 10% of list, then send winner to remaining 80%.
Content Guidelines
Do:
- ✅ Deliver value upfront
- ✅ Keep it scannable
- ✅ One main message
- ✅ Clear call-to-action
- ✅ Personable tone
Don't:
- ❌ Long walls of text
- ❌ Multiple competing CTAs
- ❌ Excessive promotion
- ❌ Clickbait
- ❌ Spam trigger words
Segmentation Strategy
Send targeted content:
Instead of:
Send "Weekly Update" to All SubscribersTry:
Send "React Tips" to React Developers segment
Send "Design Resources" to Designers segment
Send "Product Updates" to Customers segmentBetter relevance = higher engagement.
Troubleshooting
Newsletter Not Sending
Check:
- Valid API keys
- Recipients specified
- Within rate limits
- Content passes moderation
- Account in good standing
Low Open Rates
Possible causes:
- Subject line not compelling
- Sending at wrong time
- Emails going to spam
- List quality issues
- Sender reputation problems
Solutions:
- Test different subject lines
- Try different send times
- Check DNS records
- Clean your list
- Improve content quality
High Unsubscribe Rate
If above 1%, investigate:
- Content not matching expectations
- Sending too frequently
- Poor targeting
- Not enough value
Solutions:
- Survey subscribers
- Reduce frequency
- Improve segmentation
- Focus on value
Emails Going to Spam
See our comprehensive guides:
Code Examples
Node.js with Retry Logic
async function sendNewsletter(data, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(
"https://api.penna.dev/api/v1/external/newsletters/send",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-penna-public-key": process.env.PENNA_PUBLIC_KEY,
"x-penna-private-key": process.env.PENNA_PRIVATE_KEY,
},
body: JSON.stringify(data),
},
);
if (response.status === 429) {
// Rate limited, wait and retry
const waitTime = Math.pow(2, i) * 1000;
console.log(`Rate limited, waiting ${waitTime}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
continue;
}
const result = await response.json();
if (!response.ok) {
throw new Error(result.message);
}
return result;
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Attempt ${i + 1} failed, retrying...`);
}
}
}Python with Error Handling
import requests
import time
def send_newsletter(data, max_retries=3):
url = "https://api.penna.dev/api/v1/external/newsletters/send"
headers = {
"Content-Type": "application/json",
"x-penna-public-key": os.getenv("PENNA_PUBLIC_KEY"),
"x-penna-private-key": os.getenv("PENNA_PRIVATE_KEY"),
}
for attempt in range(max_retries):
try:
response = requests.post(url, json=data, headers=headers)
if response.status_code == 429:
wait_time = 2 ** attempt
print(f"Rate limited, waiting {wait_time}s...")
time.sleep(wait_time)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(1)Scheduled Sending (Node.js)
import cron from "node-cron";
// Send newsletter every Tuesday at 10 AM
cron.schedule("0 10 * * 2", async () => {
console.log("Sending weekly newsletter...");
try {
await sendNewsletter({
subject: "This week's highlights",
content: generateWeeklyContent(),
segmentIds: [],
});
console.log("Newsletter sent successfully!");
} catch (error) {
console.error("Failed to send newsletter:", error);
// Alert admin
}
});Resources
Tip: Start simple: compose, preview, test, then send. As you grow, add automation, segmentation, and optimization. The key is consistency and delivering value with every send.