Skip to main content

Overview

Plunk is a simple email service for sending transactional emails. The integration provides a straightforward way to send emails from your Go application without managing SMTP servers.
This integration is optional. Only configure Plunk if you need to send emails from your application.

Prerequisites

  1. A Plunk account - Sign up at useplunk.com
  2. API key from your Plunk dashboard
  3. Verified sender email domain (required by Plunk)

Configuration

Add your Plunk API key to your .env file:
USE_PLUNK=your_plunk_api_key_here
The API key is accessed via the configs package:
configs.GetPlunkKey()  // Returns USE_PLUNK value

Available Functions

Send Email

func SendEmailWithPlunk(payload, recipient, title, fromEmail string) error
Sends an email using the Plunk API. Parameters:
  • payload - Email body (plain text or HTML)
  • recipient - Recipient email address
  • title - Email subject line
  • fromEmail - Reply-to email address
Returns:
  • error - nil if successful, error if failed
Example:
import "backend/integrations"

err := integrations.SendEmailWithPlunk(
    "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
    "[email protected]",
    "Welcome to Our App",
    "[email protected]",
)

if err != nil {
    log.Printf("Failed to send email: %v", err)
    return err
}

Use Cases

1. Welcome Emails

Send onboarding emails to new users:
func RegisterUser(c echo.Context) error {
    // ... user registration logic ...
    
    // Send welcome email
    emailBody := fmt.Sprintf(`
        <h1>Welcome to Our App, %s!</h1>
        <p>Thanks for joining us. Here are some next steps:</p>
        <ul>
            <li>Complete your profile</li>
            <li>Explore our features</li>
            <li>Connect with others</li>
        </ul>
        <p>If you have questions, just reply to this email.</p>
    `, user.Name)
    
    err := integrations.SendEmailWithPlunk(
        emailBody,
        user.Email,
        "Welcome to Our App!",
        "[email protected]",
    )
    
    if err != nil {
        log.Printf("Failed to send welcome email: %v", err)
        // Don't fail registration if email fails
    }
    
    return c.JSON(http.StatusCreated, user)
}

2. Password Reset

Send password reset links:
func RequestPasswordReset(c echo.Context) error {
    email := c.FormValue("email")
    
    // Generate reset token
    resetToken := generateResetToken(email)
    resetLink := fmt.Sprintf("https://yourdomain.com/reset-password?token=%s", resetToken)
    
    // Send reset email
    emailBody := fmt.Sprintf(`
        <h2>Password Reset Request</h2>
        <p>Click the link below to reset your password:</p>
        <p><a href="%s">Reset Password</a></p>
        <p>This link expires in 1 hour.</p>
        <p>If you didn't request this, please ignore this email.</p>
    `, resetLink)
    
    err := integrations.SendEmailWithPlunk(
        emailBody,
        email,
        "Reset Your Password",
        "[email protected]",
    )
    
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to send reset email",
        })
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Reset email sent",
    })
}

3. Email Verification

Send verification codes:
func SendVerificationEmail(userEmail, userName, verificationCode string) error {
    emailBody := fmt.Sprintf(`
        <h1>Verify Your Email</h1>
        <p>Hi %s,</p>
        <p>Your verification code is:</p>
        <h2 style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; letter-spacing: 5px;">%s</h2>
        <p>Enter this code to verify your email address.</p>
        <p>This code expires in 15 minutes.</p>
    `, userName, verificationCode)
    
    return integrations.SendEmailWithPlunk(
        emailBody,
        userEmail,
        "Verify Your Email Address",
        "[email protected]",
    )
}

Built-in OTP Verification Helper

The scaffold includes a pre-built helper in utils/emails.go for sending OTP verification emails:
// From utils/emails.go
func SendOtpVerificationEmail(code, email, userId string) error {
    payload := fmt.Sprintf(
        "Hello, \n\nYour OTP code is %s. \n\nPlease use this code to verify your email address. \n\nThank you.", 
        code,
    )
    err := integrations.SendEmailWithPlunk(
        payload, 
        email, 
        "Welcome to My Project", 
        "[email protected]",
    )
    return err
}
Usage:
import "backend/utils"

func SendVerificationCode(c echo.Context) error {
    email := c.FormValue("email")
    userId := c.FormValue("user_id")
    
    // Generate OTP code
    code := generateOTP() // e.g., "123456"
    
    // Send via helper
    err := utils.SendOtpVerificationEmail(code, email, userId)
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error": "Failed to send verification email",
        })
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Verification code sent",
    })
}
The helper uses plain text format. For HTML emails with better styling, use the HTML example above or customize the helper function.

4. Transaction Receipts

Send payment confirmations:
func SendPaymentReceipt(order Order, user User) error {
    emailBody := fmt.Sprintf(`
        <h1>Payment Received</h1>
        <p>Hi %s,</p>
        <p>We've received your payment of <strong>%d RWF</strong>.</p>
        <h3>Order Details:</h3>
        <ul>
            <li>Order ID: %s</li>
            <li>Amount: %d RWF</li>
            <li>Date: %s</li>
            <li>Status: Completed</li>
        </ul>
        <p>Thank you for your business!</p>
    `, user.Name, order.Amount, order.ID, order.Amount, order.CreatedAt.Format("Jan 2, 2006"))
    
    return integrations.SendEmailWithPlunk(
        emailBody,
        user.Email,
        fmt.Sprintf("Receipt for Order %s", order.ID),
        "[email protected]",
    )
}

5. Notification Emails

Send alerts and updates:
func NotifyAdminOfError(errorMsg string, context string) {
    emailBody := fmt.Sprintf(`
        <h2>⚠️ Application Error</h2>
        <p><strong>Context:</strong> %s</p>
        <p><strong>Error:</strong></p>
        <pre style="background: #f4f4f4; padding: 15px; border-radius: 5px;">%s</pre>
        <p><strong>Time:</strong> %s</p>
    `, context, errorMsg, time.Now().Format(time.RFC3339))
    
    err := integrations.SendEmailWithPlunk(
        emailBody,
        "[email protected]",
        "Application Error Alert",
        "[email protected]",
    )
    
    if err != nil {
        log.Printf("Failed to send error notification: %v", err)
    }
}

HTML Email Templates

Basic Template Structure

const emailTemplate = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
        .container { max-width: 600px; margin: 0 auto; padding: 20px; }
        .header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
        .content { padding: 20px; background: #f9f9f9; }
        .button { display: inline-block; padding: 12px 30px; background: #4F46E5; color: white; text-decoration: none; border-radius: 5px; }
        .footer { text-align: center; padding: 20px; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>%s</h1>
        </div>
        <div class="content">
            %s
        </div>
        <div class="footer">
            <p>&copy; 2026 Your Company. All rights reserved.</p>
            <p>You're receiving this email because you signed up for our service.</p>
        </div>
    </div>
</body>
</html>
`

func SendStyledEmail(recipient, subject, heading, body string) error {
    htmlContent := fmt.Sprintf(emailTemplate, heading, body)
    
    return integrations.SendEmailWithPlunk(
        htmlContent,
        recipient,
        subject,
        "[email protected]",
    )
}

API Request Details

The function sends a POST request to Plunk’s API: Endpoint: https://api.useplunk.com/v1/send Headers:
Content-Type: application/json
Accept: application/json
Authorization: Bearer YOUR_API_KEY
Request Body:
{
    "to": "[email protected]",
    "subject": "Email Subject",
    "body": "<p>Email content</p>",
    "reply": "[email protected]"
}

Error Handling

The function returns errors that should be handled appropriately:
err := integrations.SendEmailWithPlunk(body, recipient, subject, from)
if err != nil {
    // Log the error
    log.Printf("Email send failed: %v", err)
    
    // Decide how to handle
    // Option 1: Fail the request
    return c.JSON(http.StatusInternalServerError, map[string]string{
        "error": "Failed to send email",
    })
    
    // Option 2: Continue anyway (for non-critical emails)
    log.Printf("Continuing despite email failure")
}
Common error scenarios:
  • Invalid API key: Check your .env configuration
  • Invalid recipient: Validate email addresses before sending
  • Rate limiting: Plunk has sending limits based on your plan
  • Network errors: API unreachable
For production applications, consider using a queue for emails:
type EmailJob struct {
    Body      string
    Recipient string
    Subject   string
    From      string
}

var emailQueue = make(chan EmailJob, 100)

func StartEmailWorker() {
    go func() {
        for job := range emailQueue {
            err := integrations.SendEmailWithPlunk(
                job.Body,
                job.Recipient,
                job.Subject,
                job.From,
            )
            
            if err != nil {
                log.Printf("Failed to send email to %s: %v", job.Recipient, err)
                // Could retry or move to dead letter queue
            } else {
                log.Printf("Email sent to %s", job.Recipient)
            }
            
            // Rate limiting: wait between sends
            time.Sleep(100 * time.Millisecond)
        }
    }()
}

func QueueEmail(body, recipient, subject, from string) {
    emailQueue <- EmailJob{
        Body:      body,
        Recipient: recipient,
        Subject:   subject,
        From:      from,
    }
}
Usage:
func main() {
    // Start email worker
    StartEmailWorker()
    
    // ... rest of app setup ...
}

func SendWelcomeEmail(user User) {
    // Queue the email instead of sending directly
    QueueEmail(
        "<h1>Welcome!</h1>",
        user.Email,
        "Welcome to Our App",
        "[email protected]",
    )
}

Testing

Test your email integration:
func TestEmailIntegration(c echo.Context) error {
    testEmail := c.QueryParam("email")
    if testEmail == "" {
        testEmail = "[email protected]"
    }
    
    err := integrations.SendEmailWithPlunk(
        "<h1>Test Email</h1><p>This is a test from Go React Scaffold.</p>",
        testEmail,
        "Test Email",
        "[email protected]",
    )
    
    if err != nil {
        return c.JSON(http.StatusInternalServerError, map[string]string{
            "error":   "Failed to send test email",
            "details": err.Error(),
        })
    }
    
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Test email sent successfully",
        "to":      testEmail,
    })
}

Best Practices

  1. Validate email addresses: Use a validation library before sending
  2. Use templates: Create reusable email templates
  3. Handle failures gracefully: Don’t fail user actions if email fails
  4. Queue emails: Don’t send emails synchronously in request handlers
  5. Test your emails: Send test emails to yourself first
  6. Respect unsubscribes: Maintain an unsubscribe list
  7. Monitor deliverability: Track bounces and complaints
  8. Use descriptive subjects: Clear subject lines improve open rates
  9. Include plain text: Some email clients prefer plain text
  10. Add unsubscribe links: Required for marketing emails

HTML Email Best Practices

  • Use inline CSS (not all email clients support <style> tags)
  • Keep width under 600px
  • Use tables for layout (more reliable than divs)
  • Test in multiple email clients
  • Include alt text for images
  • Use web-safe fonts
  • Provide a plain-text fallback

Rate Limits

Plunk has rate limits based on your plan:
  • Free tier: 3,000 emails/month
  • Paid plans: Higher limits
Monitor your usage in the Plunk dashboard.

Security Considerations

Never include sensitive data like passwords or full credit card numbers in emails.
  • Store API key securely in environment variables
  • Never commit .env file
  • Validate and sanitize email content to prevent injection
  • Use HTTPS for any links in emails
  • Don’t expose user data in email URLs
  • Implement rate limiting to prevent abuse

Troubleshooting

Emails not sending

  1. Check API key is correct in .env
  2. Verify sender domain is verified in Plunk
  3. Check Plunk dashboard for errors
  4. Look for errors in application logs
  5. Test with curl:
curl -X POST https://api.useplunk.com/v1/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "subject": "Test",
    "body": "Test message"
  }'

Emails going to spam

  • Verify your domain with Plunk
  • Add SPF and DKIM records to your DNS
  • Avoid spam trigger words
  • Include an unsubscribe link
  • Start with small volumes and increase gradually

Production Checklist

  • API key configured in production .env
  • Sender domain verified in Plunk dashboard
  • SPF/DKIM records added to DNS
  • Email templates tested in multiple clients
  • Error handling implemented
  • Email queue setup for async sending
  • Monitoring and alerting configured
  • Unsubscribe mechanism implemented
  • Rate limiting in place

Alternatives

If you need more features:
  • SendGrid: More features, higher volume
  • Postmark: Focus on deliverability
  • AWS SES: Cheaper for high volume
  • Resend: Developer-friendly, React email support
  • Mailgun: Enterprise features

Support

Build docs developers (and LLMs) love