# Iconly API — Social Graphics Skills

> Use this file to teach AI agents and LLMs how to generate social media creatives 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 Social Creative

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

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `platform` | string | — | **Required.** Target format — see Platforms below |
| `context` | string | — | **Required.** Describe the creative — product, campaign, message, tone |
| `model` | string | `"thinking"` | `"fast"` (12 tokens/format) or `"thinking"` (25 tokens/format) |
| `icon_ids` | array | `[]` | UUIDs of icons from your library to include in the design |
| `media_assets` | array | `[]` | Images to include — see Media Assets below |
| `reference_media` | object | `null` | A single image to use as visual reference (imitate a design) — see Reference Media below |
| `colors` | array | `[]` | Array of 3 hex strings: `[primary, secondary, accent]` |
| `brand_fonts` | array | `[]` | Array of Google Font names to use in the design |
| `refine_layout` | boolean | `false` | Run a second AI pass to fix text overlaps and layout issues. **Not recommended for agents** — use the Agent Review Workflow below instead for better results. |
| `num_copies` | integer | `1` | Number of independent creative variations (1–3). Only applies to single-size formats. |
| `save` | boolean | `false` | Save the creative to the user's Iconly account. If `false`, the creative is only returned in the response. |
| `name` | string | `""` | Name for the saved creative (only used when `save: true`). If named, it appears in the user's dashboard. |

### Platforms

Multi-format platforms generate 3 size variants in a single call. Single-size formats generate one creative (or up to 3 copies).

| Platform | Formats | Dimensions |
|----------|---------|------------|
| `facebook` | Feed, Square, Story | 1200×628, 1080×1080, 1080×1920 |
| `instagram` | Square, Portrait, Story | 1080×1080, 1080×1350, 1080×1920 |
| `square` | Single | 1080×1080 |
| `portrait` | Single | 1080×1350 |
| `story` | Single | 1080×1920 |
| `landscape` | Single | 1200×628 |
| `wide` | Single | 1920×1080 |
| `standard` | Single | 940×788 |

### Token Cost

Base cost per format/copy: **12** (fast) or **25** (thinking).

- Multi-format (Facebook, Instagram): 3 formats × base = 36–75 tokens
- Single-size with copies: copies × base (e.g. 3 copies × 25 = 75 tokens)
- Range: 12–75 tokens per call

### Response

```json
{
  "id": "uuid",
  "creatives": [
    {
      "format": "Square",
      "width": 1080,
      "height": 1080,
      "html": "<div style='position:relative;width:1080px;height:1080px;'>...</div>"
    }
  ],
  "text_content": "Extracted text from all creatives",
  "ad_copy": {
    "headline": "Your Headline Here",
    "primary_text": "Engaging description...",
    "cta": "Shop Now",
    "hashtags": "#brand #campaign"
  },
  "copy_fields": [
    {"key": "headline", "label": "Headline", "max_chars": 40}
  ],
  "platform": "square"
}
```

The `creatives` array contains one entry per format/copy. Each entry's `html` is a self-contained HTML snippet with inline styles, absolutely positioned elements, and Google Fonts `<link>` tags. The HTML is designed to render at the exact `width` × `height` specified.

---

## Media Assets

The `media_assets` parameter accepts an array of image objects. These are images the AI will incorporate into the creative design (product photos, logos, backgrounds, etc.).

```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 the generated HTML via `<img>` tags.

You can include up to 5 images (icons + media assets combined). If you need to include product photos, logos, or background images, pass them here.

---

## Reference Media

The `reference_media` parameter accepts a single image object to use as a visual reference. The AI will analyze the reference and try to match its layout, style, and composition.

```json
{
  "reference_media": {
    "id": "any-identifier",
    "name": "Competitor Ad",
    "url": "https://example.com/reference-ad.jpg",
    "width": 1080,
    "height": 1080
  }
}
```

Use this to imitate an existing ad, match a brand's visual style, or recreate a layout you like. The AI extracts the HTML structure from the reference when possible, then adapts it to your content. Like media assets, the URL can be any publicly accessible image — it does not need to be in your library.

---

## Working with Brands

Brands store your visual identity (colors, fonts, aesthetic, products) and can be applied to creatives for consistent branding. Brand data is passed as part of the `context` string — not as a separate parameter.

### Creating a Brand

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

Create a brand manually or with an automatic website crawl:

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

If `url` is provided, the brand is created and crawled in one step (costs 10 tokens). The crawl uses AI to extract colors, fonts, products, features, and aesthetic from the website. Without `url`, only the brand shell is created.

### Fetching Brand Data

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

Returns all your brands. Each brand object contains:

```json
{
  "id": "uuid",
  "name": "My Brand",
  "domain": "mybrand.com",
  "description": "Short brand description",
  "aesthetic": "Clean, modern, minimal with bold typography",
  "colors": {"primary": "#1a1a2e", "secondary": "#16213e", "accent": "#0f3460"},
  "fonts": ["Inter", "Playfair Display"],
  "products": ["SaaS Platform", "Mobile App"],
  "features": ["AI-powered", "Real-time analytics"],
  "logo_url": "https://..."
}
```

### Applying a Brand to a Creative

To use a brand, merge its data into the `context` field and extract its `colors` and `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" + "Launch announcement for our new dashboard feature"

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

### Using Brand Data Without a Saved Brand

You don't need a saved brand to use brand-style creatives. If you just need colors and fonts from a website, you can crawl it, extract the data, and pass it directly:

```python
# Create a temporary brand and crawl
temp = requests.post(f"{BASE}/brands/", json={
    "name": "Competitor Analysis",
    "url": "https://competitor.com"
}, headers=HEADERS).json()

brand = temp["brand"]

# Use extracted colors/fonts directly in a creative
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Summer sale promotion for outdoor furniture",
    "colors": [brand['colors']['primary'], brand['colors']['secondary'], brand['colors']['accent']],
    "brand_fonts": brand['fonts']
}, headers=HEADERS).json()
```

Or skip brands entirely and pass colors/fonts manually:

```python
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Summer sale promotion",
    "colors": ["#1a1a2e", "#e94560", "#ffffff"],
    "brand_fonts": ["Montserrat", "Open Sans"]
}, headers=HEADERS).json()
```

---

## Working with Icons

Icons from your Iconly library can be embedded in creatives. Pass their UUIDs in `icon_ids` and the AI will place them in the design.

**Important:** When generating icons for use in social creatives, 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": "rocket ship",
    "style": "line",
    "color": "#1a1a2e"
}, headers=HEADERS).json()

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

# 3. Use in a creative
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Product launch announcement",
    "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.

### List Existing Library Icons

```
GET https://iconly.ai/api/library/list/
```

Returns all icons in your library. Use the `id` field from any icon as an entry in `icon_ids`.

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

---

## Converting Creatives to Images (Required)

**The end state of social creative generation is always an image.** The generate endpoint returns HTML, but the deliverable is a rendered PNG or WebP. You must always convert the HTML to an image using the HTML Screenshot endpoint as a final step.

Use the HTML Screenshot endpoint:

```
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) |
| `height` | integer | `1080` | Viewport height (1–4096) |
| `scale` | integer | `2` | Device scale factor (1–4). `2` = 2× resolution (retina). |
| `format` | string | `"png"` | `"png"` or `"webp"` |
| `selector` | string | `null` | CSS selector to capture a specific element instead of the full page |

### Example: Generate and Export as PNG

```python
# 1. Generate creative
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Flash sale — 50% off all products this weekend"
}, headers=HEADERS).json()

# 2. Convert to image
html = creative["creatives"][0]["html"]
width = creative["creatives"][0]["width"]
height = creative["creatives"][0]["height"]

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

# 3. Save the image (response is a data URL)
import base64
data_url = screenshot["image"]
b64_data = data_url.split(",")[1]
with open("creative.png", "wb") as f:
    f.write(base64.b64decode(b64_data))
```

### Capturing a Specific Element

If the HTML contains multiple sections and you only want to capture one, use the `selector` parameter:

```python
screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": 1080,
    "height": 1080,
    "selector": "#hero-section"
}, headers=HEADERS).json()
```

---

## Timeouts

Social creative generation is a multi-step process (AI generation, optional refinement, font loading, screenshot rendering). These can be long-running processes — do not timeout before **2–3 minutes** (longer for multi-format calls like Facebook or Instagram, and longer still if using `refine_layout`). 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.

---

## Agent Review Workflow (Recommended)

If you are a multimodal AI agent, you should **review the rendered image** before delivering it rather than relying on `refine_layout`. The `refine_layout` option adds a server-side AI pass, but agent-side review produces better results because you can see and fix issues directly.

**Do not use `refine_layout` for agent workflows.** Instead, review the screenshot yourself and fix the HTML. This is faster, cheaper (no extra token cost), and more reliable.

### Recommended flow:

1. **Generate** the creative via `/api/generate-creative/` (with `refine_layout: false`, the default)
2. **Screenshot** the HTML via `/api/html-screenshot/`
3. **Review** the image — look for text overlaps, clipping, misalignment, readability issues, or elements extending outside the canvas
4. **If issues exist**, modify the HTML to fix them (adjust positions, font sizes, padding, z-index, etc.) and screenshot again
5. **Optionally refine the image** — if the rendered image still has visual issues that are hard to fix in HTML (text overlap, centering, alignment), use the Refine Creative Image endpoint to clean it up at the pixel level
6. **Deliver** the final image

This approach is especially important when the creative includes many elements (icons, media, text) that may compete for space.

```python
# 1. Generate
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Product launch announcement"
}, headers=HEADERS).json()

entry = creative["creatives"][0]
html = entry["html"]

# 2. Screenshot and review
screenshot = requests.post(f"{BASE}/html-screenshot/", json={
    "html": html,
    "width": entry["width"],
    "height": entry["height"],
    "scale": 2
}, headers=HEADERS).json()

# 3. If you are a multimodal agent, examine screenshot["image"]
#    (base64 data URL) for layout issues.
#    If the layout needs fixes, edit the HTML and re-screenshot.

# 4. Optionally refine the final image (see Refine Creative Image below)

# 5. Deliver the final image
b64_data = screenshot["image"].split(",")[1]
```

---

## Saving Creatives

By default, API calls do **not** save creatives 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 creative 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
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Product launch announcement",
    "save": True,
    "name": "Instagram Launch Campaign"
}, headers=HEADERS).json()

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

**Ask the user** if they'd like to save the creative 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 creative afterwards using the save endpoint. This avoids regenerating:

```
POST https://iconly.ai/api/social-creatives/save/
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `creatives` | array | **Required.** The `creatives` array from the generate response |
| `platform` | string | **Required.** The platform used (e.g. `"square"`, `"instagram"`) |
| `name` | string | A name for the creative (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)
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Product launch announcement"
}, headers=HEADERS).json()

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

# Save later
saved = requests.post(f"{BASE}/social-creatives/save/", json={
    "creatives": creative["creatives"],
    "platform": creative["platform"],
    "name": "Instagram Launch Campaign",
    "context": "Product launch announcement",
    "text_content": creative.get("text_content", ""),
    "ad_copy": creative.get("ad_copy", {})
}, headers=HEADERS).json()

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

### Update a Saved Creative

```
PATCH https://iconly.ai/api/social-creatives/{creative_id}/
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | A name for the creative (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 creative is accessible at `https://iconly.ai/create/social/` in the user's browser, where they can visually edit it, tweak text, reposition elements, and re-export.

### List Saved Creatives

```
GET https://iconly.ai/api/social-creatives/
```

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

### Delete a Creative

```
DELETE https://iconly.ai/api/social-creatives/{creative_id}/
```

---

## Full Pipeline Example

This example demonstrates the complete workflow: create a brand, generate icons, build a creative, and export as PNG.

```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 a matching icon set (6 tokens each)
icon_ids = []
for subject in ["dashboard", "analytics", "notifications"]:
    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 Instagram creative with brand (25 tokens × 3 formats = 75 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']}"""

creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "instagram",
    "context": brand_context + "\n\nNew feature launch — real-time analytics dashboard",
    "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 each format as PNG
for entry in creative["creatives"]:
    screenshot = requests.post(f"{BASE}/html-screenshot/", json={
        "html": entry["html"],
        "width": entry["width"],
        "height": entry["height"],
        "scale": 2
    }, headers=HEADERS).json()

    b64_data = screenshot["image"].split(",")[1]
    filename = f"instagram_{entry['format'].lower().replace(' ', '_')}.png"
    with open(filename, "wb") as f:
        f.write(base64.b64decode(b64_data))

print(f"Ad copy: {creative['ad_copy']}")
print(f"Creative ID: {creative['id']}")
```

---

## Imitating an Existing Ad

Use `reference_media` to match the style and layout of an existing ad:

```python
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Summer collection launch for eco-friendly clothing",
    "reference_media": {
        "id": "ref",
        "name": "Competitor Ad",
        "url": "https://example.com/competitor-ad.jpg",
        "width": 1080,
        "height": 1080
    },
    "colors": ["#2d5016", "#f5f0eb", "#8b6914"],
    "brand_fonts": ["Playfair Display", "Lato"]
}, headers=HEADERS).json()
```

The AI analyzes the reference image's layout, typography hierarchy, and composition, then recreates a similar design with your content and branding.

---

## Refine Creative Image

Clean up a rendered creative image at the pixel level using AI. This is an image-to-image endpoint — send in a screenshot of your creative and get back a refined version with fixed text overlaps, centering, alignment, and layout issues.

```
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 creative to refine |
| `width` | integer | `null` | Original width of the creative (helps the AI understand the layout) |
| `height` | integer | `null` | Original height of the creative (helps the AI understand the layout) |

### Response

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

### Token Cost

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

### Example: Refine a Screenshot

```python
# 1. Generate and screenshot the creative
creative = requests.post(f"{BASE}/generate-creative/", json={
    "platform": "square",
    "context": "Product launch announcement"
}, headers=HEADERS).json()

entry = creative["creatives"][0]

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

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

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

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

Use this as a final cleanup step after HTML-level fixes. It's especially useful when text overlaps or alignment issues persist after manual HTML edits.

---

## 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": 75,
  "tokens_available": 50
}
```
