penna
Guides

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

  1. Navigate to DashboardCompose or click Compose in the top navigation
  2. 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

  1. Click Preview button
  2. Review how it will look to subscribers
  3. Check both desktop and mobile views
  4. Verify all links work
  5. 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 subscribers

Penna 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:

  1. Go to Analytics (Coming Soon)
  2. View delivery status
  3. Track opens and clicks
  4. Monitor engagement
  5. 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_key

Important: 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

FieldTypeRequiredDescription
subjectstringYesEmail subject line
contentstringYesNewsletter body (supports basic markdown)
recipientEmailsstring[]No*Specific email addresses
segmentIdsstring[]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:

  1. Review your content
  2. Remove any problematic language
  3. Retry
  4. Contact support if you believe it's an error

Testing Before Sending

Send Test Email

Dashboard:

  1. Compose your newsletter
  2. Click Send Test
  3. Enter test email addresses
  4. Review the test email
  5. Make adjustments
  6. 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 list

Testing 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 Subscribers

Try:

Send "React Tips" to React Developers segment
Send "Design Resources" to Designers segment
Send "Product Updates" to Customers segment

Better relevance = higher engagement.

Troubleshooting

Newsletter Not Sending

Check:

  1. Valid API keys
  2. Recipients specified
  3. Within rate limits
  4. Content passes moderation
  5. 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.