# Iconly API — Email Templates Skills

> Use this file to teach AI agents and LLMs how to generate HTML email templates with the Iconly REST API. This skill integrates with Icon Generation, Brands, and HTML Screenshot endpoints.

## Authentication

All requests require an API key in the `X-API-Key` header. Keys use the `ik_` prefix.

```
X-API-Key: ik_your_api_key_here
```

Generate a key from **Settings > API Access** in your Iconly dashboard (Pro or Max plan required).

---

## Generate Email Template

```
POST https://iconly.ai/api/generate-email/
Content-Type: application/json
X-API-Key: ik_your_api_key_here
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `email_type` | string | — | **Required.** Template type — see Email Types below |
| `context` | string | — | **Required.** Describe the email content, purpose, and tone |
| `model` | string | `"thinking"` | `"fast"` (12 tokens) or `"thinking"` (25 tokens) |
| `icon_ids` | array | `[]` | UUIDs of icons from your library to embed in the email |
| `media_assets` | array | `[]` | Images to include — see Media Assets below |
| `reference_media` | object | `null` | A single image to use as visual reference — see Reference Media below |
| `colors` | array | `[]` | Array of 3 hex strings: `[primary, secondary, accent]` |
| `brand_fonts` | array | `[]` | Array of font names. Email-safe fonts are used directly; Google Fonts get progressive enhancement with web-safe fallbacks. |
| `save` | boolean | `false` | Save the template to the user's Iconly account. If `false`, the template is only returned in the response. |
| `name` | string | `""` | Name for the saved template (only used when `save: true`). If named, it appears in the user's dashboard. |

### Email Types

| Type | Description | Copy Fields |
|------|-------------|-------------|
| `newsletter` | Recurring content digest | Subject Line, Preheader, Section Headers |
| `promotional` | Sales, offers, campaigns | Subject Line, Preheader, Headline, CTA |
| `welcome` | New user onboarding | Subject Line, Preheader, Greeting, CTA |
| `transactional` | Order confirmations, receipts | Subject Line, Preheader |
| `announcement` | Product launches, updates | Subject Line, Preheader, Headline, CTA |
| `event_invite` | Event promotions | Subject Line, Preheader, Event Details, CTA |

### Token Cost

Base cost per template: **12** (fast) or **25** (thinking). Each call generates one email template.

### Response

```json
{
  "id": "uuid",
  "creatives": [
    {
      "format": "Newsletter",
      "width": 600,
      "html": "<!DOCTYPE html><html>...</html>"
    }
  ],
  "text_content": "Extracted text from the email",
  "ad_copy": {
    "subject_line": "Your Weekly Digest",
    "preheader": "This week's top stories...",
    "section_headers": "Featured | Trending | Updates"
  },
  "copy_fields": [
    {"key": "subject_line", "label": "Subject Line", "max_chars": 60}
  ],
  "email_type": "newsletter"
}
```

The `html` in `creatives[0]` is a complete, production-ready HTML email document. It uses **table-based layout** for email client compatibility, with all styles inline. The template is 600px wide and works in Gmail, Outlook, Apple Mail, Yahoo, and other major email clients.

### Font Handling

Email templates handle fonts differently from social creatives due to email client limitations:

- **Email-safe fonts** (Arial, Helvetica, Georgia, Times New Roman, Verdana, Tahoma) are used directly
- **Google Fonts** get progressive enhancement — loaded via `<link>` in `<head>` with web-safe fallbacks for clients that don't support them
- The AI automatically categorizes your `brand_fonts` into safe vs. Google and applies them accordingly

---

## Timeouts

Email template generation involves AI generation, font loading, and HTML construction. These can be long-running processes — do not timeout before **3 minutes**. Poll for completion every 20–30 seconds rather than blocking on a single long request. Do not retry or cancel early — the generation is still running.

---

## Media Assets

The `media_assets` parameter accepts an array of image objects. These are images the AI will incorporate into the email design.

```json
{
  "media_assets": [
    {
      "id": "any-identifier",
      "name": "Product Photo",
      "url": "https://example.com/product.jpg",
      "width": 800,
      "height": 600
    }
  ]
}
```

**Important:** Media assets do NOT need to be in your Iconly library. Any publicly accessible image URL works. The AI uses the URL directly in `<img>` tags within the email HTML.

You can include up to 5 images (icons + media assets combined).

---

## Reference Media

The `reference_media` parameter accepts a single image object to use as a visual reference. The AI analyzes the reference and tries to match its layout, typography, and design style.

```json
{
  "reference_media": {
    "id": "any-identifier",
    "name": "Reference Email",
    "url": "https://example.com/reference-email.png",
    "width": 600,
    "height": 1200
  }
}
```

Use this to replicate the style of an existing email design. The URL can be any publicly accessible image.

---

## Working with Brands

Brands store your visual identity and can be applied to email templates for consistent branding. Brand data is passed as part of the `context` string.

### Creating a Brand

```
POST https://iconly.ai/api/brands/
```

Create with automatic website crawl:

```json
{
  "name": "My Brand",
  "url": "https://mybrand.com"
}
```

The crawl extracts colors, fonts, products, features, and aesthetic (costs 10 tokens). Without `url`, only the brand shell is created.

### Applying a Brand to an Email

Merge brand data into `context` and extract colors/fonts:

```python
# 1. Fetch brand
brand = requests.get(f"{BASE}/brands/{brand_id}/",
    headers=HEADERS).json()["brand"]

# 2. Build context with brand info
brand_context = f"""Brand: {brand['name']}
Website: {brand['domain']}
Brand Info: {brand['description']}
Products/Services: {', '.join(brand['products'])}
Key Features: {', '.join(brand['features'])}
Brand Aesthetic (use as subtle guide): {brand['aesthetic']}"""

full_context = brand_context + "\n\n" + "Monthly newsletter with product updates and tips"

# 3. Generate email with brand colors and fonts
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "newsletter",
    "context": full_context,
    "colors": [brand['colors']['primary'], brand['colors']['secondary'], brand['colors']['accent']],
    "brand_fonts": brand['fonts']
}, headers=HEADERS).json()
```

### Using Colors Without a Saved Brand

You can crawl a website to extract its colors and fonts without saving a permanent brand, or just pass colors/fonts directly:

```python
# Option A: Crawl to extract, then use
temp = requests.post(f"{BASE}/brands/", json={
    "name": "Quick Extract",
    "url": "https://somesite.com"
}, headers=HEADERS).json()
colors = temp["brand"]["colors"]
fonts = temp["brand"]["fonts"]

# Option B: Pass colors/fonts manually
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "promotional",
    "context": "Black Friday sale — 40% off everything",
    "colors": ["#000000", "#ff4444", "#ffffff"],
    "brand_fonts": ["Montserrat", "Georgia"]
}, headers=HEADERS).json()
```

---

## Maintaining Consistency Across Email Templates

When a user wants to create multiple email types for the same brand (welcome, transactional, newsletter, etc.), **check for existing saved templates first** to maintain a consistent look and tone.

### Recommended flow:

1. **List existing templates** via `GET /api/email-templates/`
2. **Check** if the user already has saved templates — especially for the same brand or similar context
3. **Ask the user** if they want the new template to match the style, tone, header/footer, and copy conventions of an existing one
4. **If yes**, extract the existing template's HTML and pass it as `reference_media` (screenshot it first to get an image URL) or include style notes in the `context`

```python
# 1. Check for existing templates
existing = requests.get(f"{BASE}/email-templates/",
    headers=HEADERS).json()["creatives"]

if existing:
    # Show the user what they have
    for tmpl in existing:
        print(f"- {tmpl['name']} ({tmpl['email_type']}, created {tmpl['created_at']})")

    # Ask: "You have existing email templates. Should the new one
    #        match their style and tone?"
```

### Using an Existing Template as Reference

If the user wants consistency, take a screenshot of the existing template and pass it as `reference_media`:

```python
# 2. Screenshot an existing template for reference
ref_template = existing[0]  # the template to match
ref_html = ref_template["creatives"][0]["html"]

screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": ref_html,
    "width": 600,
    "height": 1200,
    "scale": 1,
    "format": "png"
}, headers=HEADERS).json()

# Save screenshot temporarily (or use the data URL directly if your
# agent can handle it as a reference)
ref_image_data = screenshot["image"]

# 3. Include style guidance in the context
consistency_note = f"""Match the visual style, header/footer layout, font usage, and tone of
our existing {ref_template['email_type']} template "{ref_template['name']}".
Keep the same color scheme, button styles, and spacing conventions."""

# 4. Generate new template with reference
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "welcome",
    "context": brand_context + "\n\n" + consistency_note + "\n\nWelcome email for new users",
    "colors": [brand["colors"]["primary"], brand["colors"]["secondary"], brand["colors"]["accent"]],
    "brand_fonts": brand["fonts"],
    "reference_media": {
        "id": ref_template["id"],
        "name": ref_template["name"],
        "url": ref_image_data,
        "width": 600,
        "height": 1200
    }
}, headers=HEADERS).json()
```

### Building an Email Template Suite

When creating a full set of templates (welcome, transactional, promotional, etc.), generate the first one, save it, then use it as a reference for the rest:

```python
email_types = ["welcome", "transactional", "promotional", "announcement"]
saved_ids = []
reference = None

for i, email_type in enumerate(email_types):
    payload = {
        "email_type": email_type,
        "context": brand_context + f"\n\n{email_type.title()} email for our platform",
        "colors": [brand["colors"]["primary"], brand["colors"]["secondary"], brand["colors"]["accent"]],
        "brand_fonts": brand["fonts"],
        "save": True,
        "name": f"{brand['name']} - {email_type.title()}"
    }

    # After the first template, use it as reference for the rest
    if reference:
        payload["reference_media"] = reference
        payload["context"] += "\n\nMatch the visual style and layout of our other email templates."

    email = requests.post(f"{BASE}/generate-email/", json=payload, headers=HEADERS).json()
    saved_ids.append(email["id"])

    # Screenshot the first template to use as reference
    if i == 0:
        html = email["creatives"][0]["html"]
        shot = requests.post(f"{BASE}/html-screenshot/", json={
            "html": html, "width": 600, "height": 1200, "scale": 1
        }, headers=HEADERS).json()
        reference = {
            "id": email["id"],
            "name": f"{brand['name']} - {email_type.title()}",
            "url": shot["image"],
            "width": 600,
            "height": 1200
        }
```

This produces a cohesive set of templates that share the same visual identity.

---

## Working with Icons

Icons from your Iconly library can be embedded in email templates. Pass their UUIDs in `icon_ids`.

**Important:** When generating icons for use in email templates, you **MUST always save them to the user's library** via `/api/library/add/`. The `icon_ids` parameter only accepts UUIDs of saved library icons — unsaved icon data cannot be used directly. Always generate the icon, save it, then pass the `icon_id`.

### Generate Icons First

Generate icons using the Icon Generation skill, **save to library**, then reference them:

```python
# 1. Generate an icon
icon = requests.post(f"{BASE}/generate-icon/", json={
    "subject": "envelope",
    "style": "line",
    "color": "#1a1a2e"
}, headers=HEADERS).json()

# 2. Save to library
saved = requests.post(f"{BASE}/library/add/", json={
    "name": "envelope",
    "image_data": icon["image_data"],
    "style": "line"
}, headers=HEADERS).json()

# 3. Use in email
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "welcome",
    "context": "Welcome email for new SaaS platform users",
    "icon_ids": [saved["icon_id"]]
}, headers=HEADERS).json()
```

**Shell-based agents:** The `image_data` field is ~200–500 KB of base64. Do NOT interpolate it into shell variables or `curl -d` strings — it will exceed argument limits. Write the JSON payload to a temp file and use `curl -d @file` instead. Also avoid Python's `urllib.request` (Cloudflare blocks its User-Agent). See the **"Saving Images: Handling Large Payloads"** section in the Icon Generation skills file for full details and examples.

**Note:** Unlike media assets, icons MUST be in your library. The endpoint validates `icon_ids` against your account.

---

## Converting Email Templates to Images (Optional)

**The end state of email generation is the HTML itself** — you'll paste it into Mailchimp, SendGrid, Brevo, or another email service. However, you can optionally convert the email to an image for visual review or preview purposes:

```
POST https://iconly.ai/api/html-screenshot/
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `html` | string | — | **Required.** The HTML to render |
| `width` | integer | `1080` | Viewport width (1–4096). Use `600` for emails. |
| `height` | integer | `1080` | Viewport height (1–4096) |
| `scale` | integer | `2` | Device scale factor (1–4) |
| `format` | string | `"png"` | `"png"` or `"webp"` |
| `selector` | string | `null` | CSS selector to capture a specific element |

### Example: Email Preview Image

```python
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "promotional",
    "context": "Summer clearance sale"
}, headers=HEADERS).json()

html = email["creatives"][0]["html"]

# Capture as image — use 600px width to match email width
screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": 600,
    "height": 1200,
    "scale": 2,
    "format": "png"
}, headers=HEADERS).json()

import base64
b64_data = screenshot["image"].split(",")[1]
with open("email_preview.png", "wb") as f:
    f.write(base64.b64decode(b64_data))
```

---

## Agent Review Workflow (Recommended)

**The deliverable is the HTML**, not an image. However, if you are a multimodal AI agent, taking a screenshot for visual review can help you catch layout issues that are hard to detect from raw HTML alone.

### Recommended flow:

1. **Generate** the email via `/api/generate-email/`
2. **Review the HTML** — check structure, table layout, inline styles, font fallbacks, link placement
3. **Optionally screenshot** via `/api/html-screenshot/` to visually verify the layout
4. **If issues exist**, modify the HTML directly (fix spacing, font sizes, alignment, broken table structure, etc.)
5. **Optionally refine the preview image** — if the rendered preview still has visual issues, use the Refine Creative Image endpoint (`POST /api/refine-creative-image/`) to clean it up at the pixel level (20 tokens)
6. **Deliver** the final HTML for use in the email service

Unlike social creatives (where the image is the final deliverable), email templates are consumed as HTML. The screenshot is a reference tool, not the output. Focus your review on:

- Table structure integrity (emails use `<table>` layout, not flexbox/grid)
- Inline styles on every element (email clients strip `<style>` blocks)
- Web-safe font fallbacks for Google Fonts
- Image `alt` text and link `href` values
- Responsive behavior at 600px width

```python
# 1. Generate
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "promotional",
    "context": "Black Friday sale — 40% off everything"
}, headers=HEADERS).json()

html = email["creatives"][0]["html"]

# 2. Optionally screenshot for visual review
screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": 600,
    "height": 1200,
    "scale": 2
}, headers=HEADERS).json()

# 3. If you are a multimodal agent, examine screenshot["image"]
#    to verify the layout looks correct.
#    If fixes are needed, edit the HTML directly.

# 4. Deliver the HTML
final_html = html  # or modified version
subject = email["ad_copy"].get("subject_line", "")
```

---

## Saving Email Templates

By default, API calls do **not** save templates to the user's Iconly account — the HTML and copy are returned in the response for local use. This keeps the user's dashboard clean when running pipelines.

To save a template to the user's Iconly account, pass `save: true` in the generate request. You can also include a `name` to make it immediately visible in the dashboard:

```python
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "newsletter",
    "context": "Monthly product updates and tips",
    "save": True,
    "name": "February Newsletter"
}, headers=HEADERS).json()

# email["id"] is the saved template's UUID
```

**Ask the user** if they'd like to save the template for later viewing and editing in the Iconly web editor. If yes, include `save: true` and a `name`. If not, just use the returned HTML/copy locally.

### Save After Generation

If you generated without `save: true`, you can save the template afterwards using the save endpoint. This avoids regenerating:

```
POST https://iconly.ai/api/email-templates/save/
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `creatives` | array | **Required.** The `creatives` array from the generate response |
| `email_type` | string | **Required.** The email type used (e.g. `"newsletter"`, `"promotional"`) |
| `name` | string | A name for the template (max 255 chars) |
| `context` | string | The original context used for generation |
| `text_content` | string | The `text_content` from the generate response |
| `ad_copy` | object | The `ad_copy` from the generate response |

```python
# Generate first (without saving)
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "newsletter",
    "context": "Monthly product updates and tips"
}, headers=HEADERS).json()

# ... review, fix HTML, etc. ...

# Save later
saved = requests.post(f"{BASE}/email-templates/save/", json={
    "creatives": email["creatives"],
    "email_type": email["email_type"],
    "name": "February Newsletter",
    "context": "Monthly product updates and tips",
    "text_content": email.get("text_content", ""),
    "ad_copy": email.get("ad_copy", {})
}, headers=HEADERS).json()

# saved["id"] is the saved template's UUID
```

### Update a Saved Template

```
PATCH https://iconly.ai/api/email-templates/{template_id}/
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | A name for the template (max 255 chars) |
| `creatives` | array | Updated creatives array (if you modified the HTML) |
| `text_content` | string | Updated text content |
| `ad_copy` | object | Updated copy fields |

Once saved, the template is accessible at `https://iconly.ai/create/email/` in the user's browser, where they can visually edit it, tweak text, adjust layout, and copy the final HTML.

### List Saved Templates

```
GET https://iconly.ai/api/email-templates/
```

Returns up to 50 most recent named templates with full HTML, copy, and metadata.

### Delete a Template

```
DELETE https://iconly.ai/api/email-templates/{template_id}/
```

---

## Full Pipeline Example

This example demonstrates the complete workflow: create a brand, generate icons, build an email template, and export a preview image.

```python
import requests, base64

API_KEY = "ik_your_api_key_here"
BASE = "https://iconly.ai/api"
HEADERS = {"Content-Type": "application/json", "X-API-Key": API_KEY}

# 1. Create brand with crawl (10 tokens)
brand_resp = requests.post(f"{BASE}/brands/", json={
    "name": "TechCorp",
    "url": "https://techcorp.com"
}, headers=HEADERS).json()
brand = brand_resp["brand"]

# 2. Generate icons for the email (6 tokens each)
icon_ids = []
for subject in ["rocket", "chart", "checkmark"]:
    icon = requests.post(f"{BASE}/generate-icon/", json={
        "subject": subject,
        "style": "line",
        "color": brand["colors"]["primary"]
    }, headers=HEADERS).json()

    saved = requests.post(f"{BASE}/library/add/", json={
        "name": subject,
        "image_data": icon["image_data"],
        "style": "line"
    }, headers=HEADERS).json()
    icon_ids.append(saved["icon_id"])

# 3. Generate email template (25 tokens)
brand_context = f"""Brand: {brand['name']}
Website: {brand['domain']}
Brand Info: {brand['description']}
Products/Services: {', '.join(brand['products'])}
Key Features: {', '.join(brand['features'])}
Brand Aesthetic: {brand['aesthetic']}"""

email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "announcement",
    "context": brand_context + "\n\nNew feature launch — real-time analytics dashboard with AI insights",
    "icon_ids": icon_ids,
    "colors": [brand["colors"]["primary"], brand["colors"]["secondary"], brand["colors"]["accent"]],
    "brand_fonts": brand["fonts"],
    "model": "thinking"
}, headers=HEADERS).json()

# 4. Export preview image
html = email["creatives"][0]["html"]
screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": 600,
    "height": 1200,
    "scale": 2
}, headers=HEADERS).json()

b64_data = screenshot["image"].split(",")[1]
with open("email_preview.png", "wb") as f:
    f.write(base64.b64decode(b64_data))

# 5. Use the HTML in your email service
email_html = email["creatives"][0]["html"]
subject = email["ad_copy"].get("subject_line", "")
preheader = email["ad_copy"].get("preheader", "")

print(f"Subject: {subject}")
print(f"Preheader: {preheader}")
print(f"Email ID: {email['id']}")
# Pass email_html to Mailchimp, SendGrid, Brevo, etc.
```

---

## Refine Creative Image

Clean up a rendered email preview image at the pixel level using AI. This is an image-to-image endpoint — send in a screenshot of your email and get back a refined version with fixed text overlaps, centering, alignment, and layout issues. Useful for visual review previews, not for the final HTML deliverable.

```
POST https://iconly.ai/api/refine-creative-image/
Content-Type: application/json
X-API-Key: ik_your_api_key_here
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `image_data` | string | — | **Required.** Base64-encoded PNG or JPEG of the email preview to refine |
| `width` | integer | `null` | Original width of the email (typically 600) |
| `height` | integer | `null` | Original height of the email |

### Response

```json
{
  "image_data": "base64-encoded refined image",
  "format": "png"
}
```

### Token Cost

**20 tokens** per refinement. Requires Pro/Max tier.

### Example: Refine an Email Preview

```python
# 1. Generate and screenshot the email
email = requests.post(f"{BASE}/generate-email/", json={
    "email_type": "promotional",
    "context": "Summer clearance sale"
}, headers=HEADERS).json()

html = email["creatives"][0]["html"]

screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": 600,
    "height": 1200,
    "scale": 2
}, headers=HEADERS).json()

# 2. Extract the base64 image data
image_b64 = screenshot["image"].split(",")[1]

# 3. Refine the preview image
refined = requests.post(f"{BASE}/refine-creative-image/", json={
    "image_data": image_b64,
    "width": 600,
    "height": 1200
}, headers=HEADERS).json()

# 4. Use the refined preview
import base64
with open("refined_email_preview.png", "wb") as f:
    f.write(base64.b64decode(refined["image_data"]))
```

**Note:** This refines the image preview only, not the email HTML itself. The deliverable for email templates remains the HTML — use this for visual review or generating polished preview images.

---

## Rate Limits

| Category | Limit |
|----------|-------|
| Generation | 10 requests/min |
| Screenshot | 10 requests/min |

---

## Error Handling

All errors return JSON with an `error` field.

| Status | Meaning |
|--------|---------|
| 400 | Bad request — missing or invalid parameters |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — insufficient tokens, wrong tier (Pro/Max required), or unverified account |
| 429 | Rate limited — wait and retry |
| 500 | Server error |

Token-related 403 errors include extra fields:

```json
{
  "error": "Insufficient tokens...",
  "tokens_needed": 25,
  "tokens_available": 10
}
```
