Claude Code Plugins

Community-maintained marketplace

Feedback

poplar-direct-mail

@ckorhonen/claude-skills
0
0

Design and send programmatic direct mail using Poplar's HTML templates and API. Use for creating dynamic mail pieces (postcards, bifolds, trifolds, letters) with personalization and triggering mail via API.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name poplar-direct-mail
description Design and send programmatic direct mail using Poplar's HTML templates and API. Use for creating dynamic mail pieces (postcards, bifolds, trifolds, letters) with personalization and triggering mail via API.

Poplar Direct Mail

Overview

Design professional direct mail creative using Poplar's HTML template system and send programmatic mail pieces via their API. This skill covers template creation with dynamic personalization, API integration for triggered mailings, and creative best practices for high-converting direct mail campaigns.

When to Use

  • Creating HTML templates for direct mail postcards, bi-folds, tri-folds, or letters
  • Building triggered/programmatic direct mail campaigns
  • Integrating direct mail into marketing automation workflows
  • Designing personalized mail pieces with dynamic content (names, offers, QR codes)
  • Sending transactional mail (receipts, confirmations, statements)
  • Setting up abandoned cart or win-back mail campaigns

Prerequisites

Installation

pip install requests

Set your API token:

# Test token (for development - only works with mailing endpoint)
export POPLAR_API_TOKEN="test_your_token_here"

# Production token (for live mailings)
export POPLAR_API_TOKEN="your_production_token_here"

Mail Formats & Dimensions

Postcards

Size Final Trim With Bleed Pixels (300 PPI)
4" x 6" 4" x 6" 4.25" x 6.25" 1275 x 1875
6" x 9" 6" x 9" 6.25" x 9.25" 1875 x 2775
6" x 11" 6" x 11" 6.25" x 11.25" 1875 x 3375

Best for: Retargeting, time-sensitive promotions, simple messaging, direct mail newcomers

Bi-folds

Format Pixels (300 PPI)
Short-fold (folds left to right) 1725 x 5175
Long-fold (folds top to bottom) 3375 x 2625

Best for: Prospecting campaigns, new product launches, complex value propositions

Tri-folds

Format Pixels (300 PPI)
Standard Tri-fold 2625 x 4987

Best for: Multiple product selections, detailed information, catalogs

Letters

Format Dimensions
8.5" x 11" (Color or B&W) 612 x 792 px (no bleed required)

Best for: Financial/insurance, sensitive content, transactional mailings, professional communications

HTML Template Structure

Basic Template

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap"
        rel="stylesheet" type="text/css">
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      width: 4.25in;
      height: 6.25in;
      position: relative;
      font-family: 'Open Sans', sans-serif;
    }

    .background {
      position: absolute;
      width: 100%;
      height: 100%;
      background-image: url('https://your-hosted-image.com/background.png');
      background-size: cover;
    }

    .headline {
      position: absolute;
      top: 0.5in;
      left: 0.375in;
      font-size: 28pt;
      font-weight: 700;
      color: #1a1a1a;
    }

    .personalized-text {
      position: absolute;
      top: 1.2in;
      left: 0.375in;
      font-size: 14pt;
      color: #333333;
    }

    .offer-box {
      position: absolute;
      bottom: 1in;
      left: 0.375in;
      width: 3.5in;
      padding: 0.25in;
      background-color: #ff6b35;
      color: white;
      text-align: center;
      font-size: 18pt;
      font-weight: 600;
    }
  </style>
</head>
<body>
  <div class="background"></div>
  <div class="headline">Special Offer Inside!</div>
  <div class="personalized-text">
    Hi {{recipient.first_name | default: "Friend"}},
  </div>
  <div class="offer-box">
    Use code: {{promotion.promo_code}}
  </div>
</body>
</html>

CSS Requirements

Requirement Details
CSS Location All styles in <style> tag within <head>
Positioning Use position: absolute for all elements
Units Only in (inches) or px (pixels) - no em, rem, vw, vh
Image URLs Absolute URLs only (must be publicly accessible)
Fonts Google Fonts or self-hosted .ttf/.woff files

Font Implementation

Google Fonts (recommended):

<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap"
      rel="stylesheet" type="text/css">

Custom fonts:

@font-face {
  font-family: "CustomFont";
  src: url("https://your-domain.com/fonts/CustomFont.ttf") format("truetype");
}

Merge Tags & Personalization

Recipient Data

{{recipient.first_name}}      <!-- First name or "Current Resident" -->
{{recipient.last_name}}       <!-- Last name -->
{{recipient.full_name}}       <!-- Full name -->
{{recipient.address_1}}       <!-- Street address -->
{{recipient.city}}            <!-- City -->
{{recipient.state}}           <!-- State -->
{{recipient.postal_code}}     <!-- ZIP code -->

Promotion Codes

{{promotion.promo_code}}      <!-- Unique promo code -->
{{promotion.qr_url}}          <!-- URL for QR code generation -->

Location-Based

{{location.city}}             <!-- Recipient's city -->
{{location.state}}            <!-- Recipient's state -->
{{location.store_address}}    <!-- Nearest store address -->

Custom Data

{{custom.purchase_amount}}    <!-- Custom field from your data -->
{{custom.product_name}}       <!-- Product purchased -->
{{custom.loyalty_tier}}       <!-- Customer tier -->

Liquid Template Logic

Conditional Content

{% if recipient.first_name %}
  Hi {{recipient.first_name | capitalize}},
{% else %}
  Dear Valued Customer,
{% endif %}

Offer Tiers Based on Purchase History

{% if custom.lifetime_value >= 500 %}
  <div class="offer">Enjoy 25% off your next order!</div>
{% elsif custom.lifetime_value >= 200 %}
  <div class="offer">Take 15% off your next purchase!</div>
{% else %}
  <div class="offer">Get $10 off orders over $50!</div>
{% endif %}

Rolling Expiration Dates

<!-- 30 days from print date -->
Expires: {{ "now" | date: "%s" | plus: 2592000 | date: "%B %e, %Y" }}

<!-- 90 days from print date -->
Expires: {{ "now" | date: "%s" | plus: 7776000 | date: "%B %e, %Y" }}

Text Formatting Filters

{{ recipient.first_name | capitalize }}     <!-- Capitalizes first letter -->
{{ recipient.first_name | upcase }}         <!-- ALL CAPS -->
{{ recipient.first_name | downcase }}       <!-- all lowercase -->
{{ custom.price | money }}                  <!-- Format as currency -->

API Reference

Authentication

All requests require a Bearer token:

curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     https://api.heypoplar.com/v1/me

Create Mailing

Endpoint: POST https://api.heypoplar.com/v1/mailing

curl -X POST https://api.heypoplar.com/v1/mailing \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": "your-campaign-id",
    "creative_id": "your-creative-id",
    "recipient": {
      "first_name": "Jane",
      "last_name": "Smith",
      "address_1": "123 Main Street",
      "address_2": "Apt 4B",
      "city": "San Francisco",
      "state": "CA",
      "postal_code": "94102"
    },
    "merge_tags": {
      "promo_code": "SAVE20",
      "expiration_date": "December 31, 2024"
    }
  }'

Response (201 Created):

{
  "id": "mailing-uuid",
  "campaign_id": "campaign-uuid",
  "creative_id": "creative-uuid",
  "state": "processing",
  "front_url": "https://app.heypoplar.com/preview/front.png",
  "back_url": "https://app.heypoplar.com/preview/back.png",
  "pdf_url": "https://app.heypoplar.com/preview/proof.pdf",
  "total_cost": "0.89",
  "created_at": "2024-01-15T10:30:00Z"
}

Schedule Future Mailing

curl -X POST https://api.heypoplar.com/v1/mailing \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_id": "your-campaign-id",
    "recipient": {
      "full_name": "John Doe",
      "address_1": "456 Oak Avenue",
      "city": "Austin",
      "state": "TX",
      "postal_code": "78701"
    },
    "send_at": "2024-02-01T09:00:00Z"
  }'

Get Mailing Status

Endpoint: GET https://api.heypoplar.com/v1/mailing/:id

curl https://api.heypoplar.com/v1/mailing/MAILING_ID \
  -H "Authorization: Bearer YOUR_TOKEN"

List Campaigns

Endpoint: GET https://api.heypoplar.com/v1/campaigns

curl https://api.heypoplar.com/v1/campaigns \
  -H "Authorization: Bearer YOUR_TOKEN"

List Campaign Creatives

Endpoint: GET https://api.heypoplar.com/v1/campaign/:id/creatives

curl https://api.heypoplar.com/v1/campaign/CAMPAIGN_ID/creatives \
  -H "Authorization: Bearer YOUR_TOKEN"

Add to Do Not Mail List

Endpoint: POST https://api.heypoplar.com/v1/do-not-mail

curl -X POST https://api.heypoplar.com/v1/do-not-mail \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "customer@example.com",
    "address": {
      "address_1": "123 Main St",
      "postal_code": "94102"
    }
  }'

Python Scripts

Send Single Mailing

#!/usr/bin/env python3
"""Send a single direct mail piece via Poplar API."""

import os
import requests
import argparse

POPLAR_API_URL = "https://api.heypoplar.com/v1"

def send_mailing(
    campaign_id: str,
    recipient: dict,
    creative_id: str = None,
    merge_tags: dict = None,
    send_at: str = None
) -> dict:
    """Send a mailing via Poplar API."""
    token = os.environ.get("POPLAR_API_TOKEN")
    if not token:
        raise ValueError("POPLAR_API_TOKEN environment variable not set")

    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    payload = {
        "campaign_id": campaign_id,
        "recipient": recipient
    }

    if creative_id:
        payload["creative_id"] = creative_id
    if merge_tags:
        payload["merge_tags"] = merge_tags
    if send_at:
        payload["send_at"] = send_at

    response = requests.post(
        f"{POPLAR_API_URL}/mailing",
        headers=headers,
        json=payload
    )
    response.raise_for_status()
    return response.json()


def main():
    parser = argparse.ArgumentParser(description="Send a Poplar direct mail piece")
    parser.add_argument("--campaign-id", required=True, help="Campaign ID")
    parser.add_argument("--creative-id", help="Creative ID (optional)")
    parser.add_argument("--first-name", required=True, help="Recipient first name")
    parser.add_argument("--last-name", required=True, help="Recipient last name")
    parser.add_argument("--address", required=True, help="Street address")
    parser.add_argument("--city", required=True, help="City")
    parser.add_argument("--state", required=True, help="State (2-letter code)")
    parser.add_argument("--zip", required=True, help="ZIP code")
    parser.add_argument("--promo-code", help="Promo code merge tag")

    args = parser.parse_args()

    recipient = {
        "first_name": args.first_name,
        "last_name": args.last_name,
        "address_1": args.address,
        "city": args.city,
        "state": args.state,
        "postal_code": args.zip
    }

    merge_tags = {}
    if args.promo_code:
        merge_tags["promo_code"] = args.promo_code

    result = send_mailing(
        campaign_id=args.campaign_id,
        recipient=recipient,
        creative_id=args.creative_id,
        merge_tags=merge_tags if merge_tags else None
    )

    print(f"Mailing created: {result['id']}")
    print(f"Status: {result['state']}")
    print(f"Cost: ${result['total_cost']}")
    print(f"PDF Preview: {result['pdf_url']}")


if __name__ == "__main__":
    main()

Batch Send from CSV

#!/usr/bin/env python3
"""Send batch mailings from a CSV file."""

import os
import csv
import time
import requests
import argparse
from typing import Generator

POPLAR_API_URL = "https://api.heypoplar.com/v1"

def read_recipients(csv_path: str) -> Generator[dict, None, None]:
    """Read recipients from CSV file."""
    with open(csv_path, 'r') as f:
        reader = csv.DictReader(f)
        for row in reader:
            yield {
                "recipient": {
                    "first_name": row.get("first_name", ""),
                    "last_name": row.get("last_name", ""),
                    "address_1": row.get("address_1", ""),
                    "address_2": row.get("address_2", ""),
                    "city": row.get("city", ""),
                    "state": row.get("state", ""),
                    "postal_code": row.get("postal_code", "")
                },
                "merge_tags": {
                    k: v for k, v in row.items()
                    if k not in ["first_name", "last_name", "address_1",
                                 "address_2", "city", "state", "postal_code"]
                }
            }


def send_batch(
    csv_path: str,
    campaign_id: str,
    creative_id: str = None,
    delay: float = 0.1
) -> tuple[int, int]:
    """Send batch mailings from CSV."""
    token = os.environ.get("POPLAR_API_TOKEN")
    if not token:
        raise ValueError("POPLAR_API_TOKEN environment variable not set")

    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    success_count = 0
    error_count = 0

    for record in read_recipients(csv_path):
        payload = {
            "campaign_id": campaign_id,
            "recipient": record["recipient"]
        }

        if creative_id:
            payload["creative_id"] = creative_id
        if record["merge_tags"]:
            payload["merge_tags"] = record["merge_tags"]

        try:
            response = requests.post(
                f"{POPLAR_API_URL}/mailing",
                headers=headers,
                json=payload
            )
            response.raise_for_status()
            success_count += 1
            print(f"Sent to {record['recipient']['first_name']} {record['recipient']['last_name']}")
        except requests.exceptions.RequestException as e:
            error_count += 1
            print(f"Error sending to {record['recipient']}: {e}")

        time.sleep(delay)

    return success_count, error_count


def main():
    parser = argparse.ArgumentParser(description="Send batch Poplar mailings from CSV")
    parser.add_argument("--csv", required=True, help="Path to CSV file")
    parser.add_argument("--campaign-id", required=True, help="Campaign ID")
    parser.add_argument("--creative-id", help="Creative ID (optional)")
    parser.add_argument("--delay", type=float, default=0.1, help="Delay between requests (seconds)")

    args = parser.parse_args()

    success, errors = send_batch(
        csv_path=args.csv,
        campaign_id=args.campaign_id,
        creative_id=args.creative_id,
        delay=args.delay
    )

    print(f"\nCompleted: {success} sent, {errors} errors")


if __name__ == "__main__":
    main()

Test API Connection

#!/usr/bin/env python3
"""Test Poplar API connection and list campaigns."""

import os
import requests

POPLAR_API_URL = "https://api.heypoplar.com/v1"

def test_connection():
    """Test API connection and list available campaigns."""
    token = os.environ.get("POPLAR_API_TOKEN")
    if not token:
        print("Error: POPLAR_API_TOKEN environment variable not set")
        return False

    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json"
    }

    # Test authentication
    print("Testing API connection...")
    try:
        response = requests.get(f"{POPLAR_API_URL}/me", headers=headers)
        response.raise_for_status()
        org = response.json()
        print(f"Connected to organization: {org.get('name', 'Unknown')}")
    except requests.exceptions.RequestException as e:
        print(f"Authentication failed: {e}")
        return False

    # List campaigns
    print("\nAvailable campaigns:")
    try:
        response = requests.get(f"{POPLAR_API_URL}/campaigns", headers=headers)
        response.raise_for_status()
        campaigns = response.json()

        if not campaigns:
            print("  No campaigns found")
        else:
            for campaign in campaigns:
                print(f"  - {campaign.get('name', 'Unnamed')}: {campaign.get('id')}")
    except requests.exceptions.RequestException as e:
        print(f"Failed to list campaigns: {e}")
        return False

    return True


if __name__ == "__main__":
    test_connection()

Design Best Practices

Visual Design

  • Less is more: Avoid cluttering with excessive information
  • High contrast: Use contrasting colors for text visibility
  • Large fonts: Minimum 10pt for body text, 24pt+ for headlines
  • Quality images: Always use 300 PPI/DPI resolution
  • Edge-to-edge design: Extend backgrounds to bleed area

Messaging

  • Single clear CTA: One primary call-to-action per piece
  • Prominent offers: Display promotions on both front and back
  • Short URLs: Use simplified URLs (brand.com/save20)
  • Social proof: Include testimonials when relevant
  • Urgency: Add expiration dates for time-sensitive offers

QR Codes

QR code images must be pre-generated and provided as a URL in your merge tags. Poplar does not auto-generate QR codes.

Providing QR codes:

merge_tags = {
    "promotion": {
        "qr_url": "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://yoursite.com/offer123"
    }
}

Or use services like:

Design guidelines:

  • Minimum size: 0.75" x 0.75" (larger for complex URLs)
  • Quiet zone: Leave white space around the code
  • Test before printing: Always verify QR codes work
  • Use URL shorteners: Shorter URLs = simpler QR codes
  • Uppercase URLs: ALL CAPS creates smaller, more readable codes

Address Block

  • Auto-applied: Poplar adds the address block automatically
  • Design behind it: Extend your background to cover the address area
  • Don't include: Never add your own address block to templates

Template Examples

4x6 Postcard - Front (Promotional)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;800&display=swap" rel="stylesheet">
  <style>
    body {
      width: 4.25in;
      height: 6.25in;
      position: relative;
      font-family: 'Poppins', sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      margin: 0;
    }

    .safe-zone {
      position: absolute;
      top: 0.25in;
      left: 0.25in;
      right: 0.25in;
      bottom: 0.25in;
    }

    .logo {
      position: absolute;
      top: 0;
      left: 0;
      width: 1.5in;
    }

    .headline {
      position: absolute;
      top: 1in;
      left: 0;
      right: 0;
      font-size: 32pt;
      font-weight: 800;
      color: white;
      text-align: center;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }

    .subhead {
      position: absolute;
      top: 1.8in;
      left: 0;
      right: 0;
      font-size: 16pt;
      color: rgba(255,255,255,0.9);
      text-align: center;
    }

    .offer-badge {
      position: absolute;
      top: 2.5in;
      left: 50%;
      transform: translateX(-50%);
      width: 2.5in;
      height: 2.5in;
      background: white;
      border-radius: 50%;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      box-shadow: 0 10px 30px rgba(0,0,0,0.3);
    }

    .offer-amount {
      font-size: 48pt;
      font-weight: 800;
      color: #764ba2;
      line-height: 1;
    }

    .offer-text {
      font-size: 14pt;
      color: #333;
      text-transform: uppercase;
      letter-spacing: 2px;
    }
  </style>
</head>
<body>
  <div class="safe-zone">
    <img src="https://your-domain.com/logo-white.svg" class="logo" alt="Logo">
    <div class="headline">{{recipient.first_name | capitalize}}, You're Invited!</div>
    <div class="subhead">Exclusive offer just for you</div>
    <div class="offer-badge">
      <div class="offer-amount">25%</div>
      <div class="offer-text">Off Everything</div>
    </div>
  </div>
</body>
</html>

4x6 Postcard - Back (with QR Code)

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
  <style>
    body {
      width: 4.25in;
      height: 6.25in;
      position: relative;
      font-family: 'Poppins', sans-serif;
      background: white;
      margin: 0;
    }

    .content-area {
      position: absolute;
      top: 0.25in;
      left: 0.25in;
      width: 3.25in;
      bottom: 0.25in;
    }

    .details {
      position: absolute;
      top: 0;
      left: 0;
      font-size: 11pt;
      color: #333;
      line-height: 1.6;
    }

    .promo-code-box {
      position: absolute;
      top: 1.5in;
      left: 0;
      background: #f8f8f8;
      border: 2px dashed #764ba2;
      padding: 0.15in 0.25in;
      text-align: center;
    }

    .promo-label {
      font-size: 10pt;
      color: #666;
      text-transform: uppercase;
    }

    .promo-code {
      font-size: 18pt;
      font-weight: 600;
      color: #764ba2;
      letter-spacing: 3px;
    }

    .expiry {
      position: absolute;
      top: 2.4in;
      left: 0;
      font-size: 9pt;
      color: #999;
    }

    .qr-section {
      position: absolute;
      bottom: 0;
      left: 0;
    }

    .qr-code {
      width: 1in;
      height: 1in;
    }

    .qr-text {
      font-size: 9pt;
      color: #666;
      margin-top: 0.1in;
    }

    /* Address block area - leave clear for Poplar */
    .address-area {
      position: absolute;
      top: 0.375in;
      right: 0.25in;
      width: 2.625in;
      height: 1.125in;
      /* This area will be covered by Poplar's address block */
    }
  </style>
</head>
<body>
  <div class="content-area">
    <div class="details">
      Shop our entire collection with your<br>
      exclusive discount. Free shipping on<br>
      orders over $50!
    </div>

    <div class="promo-code-box">
      <div class="promo-label">Your Code</div>
      <div class="promo-code">{{promotion.promo_code}}</div>
    </div>

    <div class="expiry">
      Expires: {{ "now" | date: "%s" | plus: 2592000 | date: "%B %e, %Y" }}
    </div>

    <div class="qr-section">
      <img src="{{promotion.qr_url}}" class="qr-code" alt="Scan to shop">
      <div class="qr-text">Scan to shop now</div>
    </div>
  </div>
</body>
</html>

Troubleshooting

Template Issues

"Unsupported unit" error

  • Use only in or px units, not em, rem, vw, vh, or %

Images not appearing

  • Ensure all image URLs are absolute (full URLs starting with https://)
  • Verify URLs are publicly accessible (test in incognito browser)
  • Check file size is under 5MB

Fonts not rendering

  • Use Google Fonts or self-hosted font files
  • Typekit and Adobe Fonts are not supported
  • Include all font weights you use in the <link> tag

Merge tags not replaced

  • Check column headers in your data match merge tag names exactly
  • Verify Liquid syntax is correct (no typos)
  • Use | default: "fallback" for optional fields

API Issues

401 Unauthorized

  • Verify your API token is correct
  • Check if using test token for non-mailing endpoints (test tokens only work for /mailing)

400 Bad Request

  • Campaign must be active
  • Creative must be uploaded
  • Required recipient fields: address_1, city, state, postal_code

Campaign not active error

  • Activate the campaign in the Poplar dashboard before sending

Preview Differences

Browser vs PDF preview differ

  • Always check the PDF proof from Poplar, not browser preview
  • HTML-to-PDF conversion may render some CSS differently
  • Avoid CSS features not well-supported in PDF rendering

Resources